diff --git a/CHANGELOG.md b/CHANGELOG.md index eb13f433f6..42dc158a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/docs/de/user/repo/assets/create-repository.png b/docs/de/user/repo/assets/create-repository.png index b73d8b5648..2f314dcef5 100644 Binary files a/docs/de/user/repo/assets/create-repository.png and b/docs/de/user/repo/assets/create-repository.png differ diff --git a/docs/de/user/repo/assets/import-repository.png b/docs/de/user/repo/assets/import-repository.png new file mode 100644 index 0000000000..0932484a73 Binary files /dev/null and b/docs/de/user/repo/assets/import-repository.png differ diff --git a/docs/de/user/repo/assets/repository-overview.png b/docs/de/user/repo/assets/repository-overview.png index 2454f625a3..5cb093f65a 100644 Binary files a/docs/de/user/repo/assets/repository-overview.png and b/docs/de/user/repo/assets/repository-overview.png differ diff --git a/docs/de/user/repo/index.md b/docs/de/user/repo/index.md index 89b0b19eae..aea842f5a7 100644 --- a/docs/de/user/repo/index.md +++ b/docs/de/user/repo/index.md @@ -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 erstellen](assets/create-repository.png) +### 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 importieren](assets/import-repository.png) + + ### 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. diff --git a/docs/en/user/repo/assets/create-repository.png b/docs/en/user/repo/assets/create-repository.png index b9c99987fb..1d1874a0b3 100644 Binary files a/docs/en/user/repo/assets/create-repository.png and b/docs/en/user/repo/assets/create-repository.png differ diff --git a/docs/en/user/repo/assets/import-repository.png b/docs/en/user/repo/assets/import-repository.png new file mode 100644 index 0000000000..2a518d1302 Binary files /dev/null and b/docs/en/user/repo/assets/import-repository.png differ diff --git a/docs/en/user/repo/assets/repository-overview.png b/docs/en/user/repo/assets/repository-overview.png index 7e6bfcbab7..813b19ae5b 100644 Binary files a/docs/en/user/repo/assets/repository-overview.png and b/docs/en/user/repo/assets/repository-overview.png differ diff --git a/docs/en/user/repo/index.md b/docs/en/user/repo/index.md index 222a879543..c078777634 100644 --- a/docs/en/user/repo/index.md +++ b/docs/en/user/repo/index.md @@ -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. ![Create Repository](assets/create-repository.png) +### 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. + +![Import Repository](assets/import-repository.png) + ### 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. diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstactImportHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstactImportHandler.java index 9b5450144f..d6d4b6f127 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstactImportHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstactImportHandler.java @@ -42,7 +42,9 @@ import java.util.List; * * @author Sebastian Sdorra * @since 1.12 + * @deprecated */ +@Deprecated public abstract class AbstactImportHandler implements AdvancedImportHandler { diff --git a/scm-core/src/main/java/sonia/scm/repository/AdvancedImportHandler.java b/scm-core/src/main/java/sonia/scm/repository/AdvancedImportHandler.java index 2996270f15..80c5840cb2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AdvancedImportHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AdvancedImportHandler.java @@ -31,7 +31,9 @@ package sonia.scm.repository; * * @author Sebastian Sdorra * @since 1.43 + * @deprecated */ +@Deprecated public interface AdvancedImportHandler extends ImportHandler { diff --git a/scm-core/src/main/java/sonia/scm/repository/ImportHandler.java b/scm-core/src/main/java/sonia/scm/repository/ImportHandler.java index f9ac2214b8..a4280048c7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/ImportHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/ImportHandler.java @@ -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 importRepositories(RepositoryManager manager) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/ImportResult.java b/scm-core/src/main/java/sonia/scm/repository/ImportResult.java index d86c4b4e96..62b38ede79 100644 --- a/scm-core/src/main/java/sonia/scm/repository/ImportResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/ImportResult.java @@ -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 { /** diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java index f577993056..068380e93c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java @@ -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; /** diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryImportEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryImportEvent.java new file mode 100644 index 0000000000..a8fefefef0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryImportEvent.java @@ -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; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index 24d7037b08..ba5ff44ce2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -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 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 afterCreation) { + Repository newRepository = create(repository); + afterCreation.accept(newRepository); + return newRepository; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ImportFailedException.java b/scm-core/src/main/java/sonia/scm/repository/api/ImportFailedException.java new file mode 100644 index 0000000000..1d73104944 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ImportFailedException.java @@ -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 context, String message, Exception cause) { + super(context, message, cause); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java index 8285661b12..a86b66c899 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java @@ -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(); } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RemoteCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/RemoteCommandRequest.java index d2883e85a7..a95c3d02b5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RemoteCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RemoteCommandRequest.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/security/AuthorizationChangedEvent.java b/scm-core/src/main/java/sonia/scm/security/AuthorizationChangedEvent.java index 627bc11658..b4eb524669 100644 --- a/scm-core/src/main/java/sonia/scm/security/AuthorizationChangedEvent.java +++ b/scm-core/src/main/java/sonia/scm/security/AuthorizationChangedEvent.java @@ -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; } /** diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitImportHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitImportHandler.java index afaace217d..068ce13720 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitImportHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitImportHandler.java @@ -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; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java index 422391fd19..0a76a095d4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java @@ -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 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; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java index 6273505cf9..9f7c0d60e8 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgImportHandler.java @@ -41,7 +41,9 @@ import java.io.IOException; /** * * @author Sebastian Sdorra + * @deprecated */ +@Deprecated public class HgImportHandler extends AbstactImportHandler { diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnImportHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnImportHandler.java index 000e62fbd1..f1d24da9a8 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnImportHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnImportHandler.java @@ -27,7 +27,9 @@ package sonia.scm.repository; /** * * @author Sebastian Sdorra + * @deprecated */ +@Deprecated public class SvnImportHandler extends AbstactImportHandler { diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index eb97f1ef92..2a767ee4ee 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -46248,6 +46248,7 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Default 1`] = ` > void; }; class InputField extends React.Component { @@ -61,6 +62,12 @@ class InputField extends React.Component { this.props.onChange(event.target.value, this.props.name); }; + handleBlur = (event: FocusEvent) => { + if (this.props.onBlur) { + this.props.onBlur(event.target.value, this.props.name); + } + }; + handleKeyPress = (event: KeyboardEvent) => { const onReturnPressed = this.props.onReturnPressed; if (!onReturnPressed) { @@ -102,6 +109,7 @@ class InputField extends React.Component { onChange={this.handleInput} onKeyPress={this.handleKeyPress} disabled={disabled} + onBlur={this.handleBlur} {...createAttributesForTesting(testId)} /> diff --git a/scm-ui/ui-components/src/layout/Page.tsx b/scm-ui/ui-components/src/layout/Page.tsx index b294a10969..59401c2e0d 100644 --- a/scm-ui/ui-components/src/layout/Page.tsx +++ b/scm-ui/ui-components/src/layout/Page.tsx @@ -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 { diff --git a/scm-ui/ui-components/src/validation.test.ts b/scm-ui/ui-components/src/validation.test.ts index 8ebd50e16a..a1ca93b4f8 100644 --- a/scm-ui/ui-components/src/validation.test.ts +++ b/scm-ui/ui-components/src/validation.test.ts @@ -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); + }); + } +}); diff --git a/scm-ui/ui-components/src/validation.ts b/scm-ui/ui-components/src/validation.ts index 9e2b372dcb..ac3d953343 100644 --- a/scm-ui/ui-components/src/validation.ts +++ b/scm-ui/ui-components/src/validation.ts @@ -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); +}; diff --git a/scm-ui/ui-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts index 02d7085892..8353bf34a6 100644 --- a/scm-ui/ui-types/src/Repositories.ts +++ b/scm-ui/ui-types/src/Repositories.ts @@ -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; diff --git a/scm-ui/ui-types/src/RepositoryTypes.ts b/scm-ui/ui-types/src/RepositoryTypes.ts index c1a3694fb2..cad6546573 100644 --- a/scm-ui/ui-types/src/RepositoryTypes.ts +++ b/scm-ui/ui-types/src/RepositoryTypes.ts @@ -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 & { diff --git a/scm-ui/ui-types/src/index.ts b/scm-ui/ui-types/src/index.ts index da97204e1a..6af9803cac 100644 --- a/scm-ui/ui-types/src/index.ts +++ b/scm-ui/ui-types/src/index.ts @@ -35,7 +35,8 @@ export { RepositoryGroup, RepositoryCreation, Namespace, - NamespaceCollection + NamespaceCollection, + RepositoryUrlImport } from "./Repositories"; export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes"; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 318e7494fd..4ea48d7ba0 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -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": { diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 363a062904..5019f15bb7 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -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": { diff --git a/scm-ui/ui-webapp/src/containers/Main.tsx b/scm-ui/ui-webapp/src/containers/Main.tsx index e0af6ee867..965610d668 100644 --- a/scm-ui/ui-webapp/src/containers/Main.tsx +++ b/scm-ui/ui-webapp/src/containers/Main.tsx @@ -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 { - + + diff --git a/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx b/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx new file mode 100644 index 0000000000..9611d48e28 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx @@ -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 = ({ 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 ( + + + + + + onChange({ ...repository, username })} + value={repository.username} + helpText={t("help.usernameHelpText")} + disabled={disabled} + /> + + + onChange({ ...repository, password })} + value={repository.password} + type="password" + helpText={t("help.passwordHelpText")} + disabled={disabled} + /> + + + ); +}; + +export default ImportFromUrlForm; diff --git a/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx new file mode 100644 index 0000000000..2f50c8dd59 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryFromUrl.tsx @@ -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 = ({ url, repositoryType, setImportPending }) => { + const [repo, setRepo] = useState({ + 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(); + 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) => { + 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 ( +
+ + setValid({ ...valid, importUrl })} + disabled={loading} + /> +
+ >} + setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })} + disabled={loading} + /> + >} + disabled={loading} + setValid={(contact: boolean) => setValid({ ...valid, contact })} + /> + } + /> + + ); +}; + +export default ImportRepositoryFromUrl; diff --git a/scm-ui/ui-webapp/src/repos/components/ImportRepositoryTypeSelect.tsx b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryTypeSelect.tsx new file mode 100644 index 0000000000..22cdd3f1b3 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/ImportRepositoryTypeSelect.tsx @@ -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 = ({ 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 ( +