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 870dd175ac..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
@@ -101,7 +101,6 @@ public final class PullCommandBuilder {
//J+
URL remoteUrl = new URL(url);
-// request.reset();
request.setRemoteUrl(remoteUrl);
logger.info("pull changes from url {}", url);
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 dce69a4ef2..f2fd0d93d8 100644
--- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
+++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
@@ -46588,6 +46588,12 @@ exports[`Storyshots Forms|DropDown Add preselect if missing in options 1`] = `
>
C
+
`;
@@ -46619,12 +46625,6 @@ exports[`Storyshots Forms|DropDown Default 1`] = `
>
es
-
`;
@@ -46656,6 +46656,12 @@ exports[`Storyshots Forms|DropDown With Translation 1`] = `
>
The Meaning Of Liff
+
`;
diff --git a/scm-ui/ui-components/src/forms/DropDown.tsx b/scm-ui/ui-components/src/forms/DropDown.tsx
index bf4701ac43..e801a0c2a8 100644
--- a/scm-ui/ui-components/src/forms/DropDown.tsx
+++ b/scm-ui/ui-components/src/forms/DropDown.tsx
@@ -42,7 +42,7 @@ class DropDown extends React.Component {
render() {
const { options, optionValues, preselectedOption, className, disabled } = this.props;
- if (preselectedOption && options.some(o => o === preselectedOption)) {
+ if (preselectedOption && options.filter(o => o === preselectedOption).length === 0) {
options.push(preselectedOption);
}
diff --git a/scm-ui/ui-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts
index f89579e67e..8353bf34a6 100644
--- a/scm-ui/ui-types/src/Repositories.ts
+++ b/scm-ui/ui-types/src/Repositories.ts
@@ -39,8 +39,8 @@ export type RepositoryCreation = Repository & {
contextEntries: { [key: string]: any };
};
-export type RepositoryImport = Repository & {
- importUrl?: string;
+export type RepositoryUrlImport = Repository & {
+ importUrl: string;
username?: string;
password?: string;
};
diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.test.ts b/scm-ui/ui-webapp/src/repos/modules/repos.test.ts
index 32299037ea..10482952aa 100644
--- a/scm-ui/ui-webapp/src/repos/modules/repos.test.ts
+++ b/scm-ui/ui-webapp/src/repos/modules/repos.test.ts
@@ -59,6 +59,8 @@ import reducer, {
getPermissionsLink,
getRepository,
getRepositoryCollection,
+ IMPORT_REPO_PENDING,
+ IMPORT_REPO_SUCCESS,
isAbleToCreateRepos,
isCreateRepoPending,
isDeleteRepoPending,
@@ -69,9 +71,10 @@ import reducer, {
MODIFY_REPO_FAILURE,
MODIFY_REPO_PENDING,
MODIFY_REPO_SUCCESS,
- modifyRepo
+ modifyRepo,
+ importRepoFromUrl
} from "./repos";
-import { Repository, RepositoryCollection } from "@scm-manager/ui-types";
+import { Repository, RepositoryCollection, Link } from "@scm-manager/ui-types";
const hitchhikerPuzzle42: Repository = {
contact: "fourtytwo@hitchhiker.com",
@@ -411,6 +414,70 @@ describe("repos fetch", () => {
});
});
+ it("should successfully import repo hitchhiker/restatend", () => {
+ const importUrl = REPOS_URL + "/import/git/url";
+ const importRequest = {
+ ...hitchhikerRestatend,
+ url: "https://scm-manager.org/scm/repo/secret/puzzle42",
+ username: "trillian",
+ password: "secret"
+ };
+
+ fetchMock.postOnce(importUrl, {
+ status: 201,
+ headers: {
+ location: "repositories/hitchhiker/restatend"
+ }
+ });
+
+ fetchMock.getOnce(REPOS_URL + "/hitchhiker/restatend", hitchhikerRestatend);
+
+ const expectedActions = [
+ {
+ type: IMPORT_REPO_PENDING
+ },
+ {
+ type: IMPORT_REPO_SUCCESS
+ }
+ ];
+
+ const store = mockStore({});
+ return store.dispatch(importRepoFromUrl(URL, importRequest)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+
+ it("should successfully import repo hitchhiker/restatend and call the callback", () => {
+ const importUrl = REPOS_URL + "/import/git/url";
+ const importRequest = {
+ ...hitchhikerRestatend,
+ url: "https://scm-manager.org/scm/repo/secret/puzzle42",
+ username: "trillian",
+ password: "secret"
+ };
+
+ fetchMock.postOnce(importUrl, {
+ status: 201,
+ headers: {
+ location: "repositories/hitchhiker/restatend"
+ }
+ });
+
+ fetchMock.getOnce(REPOS_URL + "/hitchhiker/restatend", hitchhikerRestatend);
+
+ let callMe = "not yet";
+
+ const callback = (r: any) => {
+ expect(r).toEqual(hitchhikerRestatend);
+ callMe = "yeah";
+ };
+
+ const store = mockStore({});
+ return store.dispatch(importRepoFromUrl(URL, importRequest, callback)).then(() => {
+ expect(callMe).toBe("yeah");
+ });
+ });
+
it("should successfully create repo slarti/fjords", () => {
fetchMock.postOnce(REPOS_URL, {
status: 201,
@@ -431,7 +498,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 +521,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 +532,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 +597,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 +620,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 +649,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
});
diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts
index fa0aa0a92e..923f072687 100644
--- a/scm-ui/ui-webapp/src/repos/modules/repos.ts
+++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts
@@ -244,7 +244,8 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error):
// import repo
export function importRepoFromUrl(link: string, repository: RepositoryImport, callback?: (repo: Repository) => void) {
- const importLink = link + `import/${repository.type}/url`;
+ const baseLink = link.endsWith("/") ? link : link + "/";
+ const importLink = baseLink + `import/${repository.type}/url`;
return function(dispatch: any) {
dispatch(importRepoPending());
return apiClient
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java
index 834fac7c9b..6ba588abdc 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java
@@ -28,6 +28,7 @@ import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Resources;
+import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.jboss.resteasy.mock.MockHttpRequest;
@@ -42,11 +43,16 @@ import org.mockito.Mock;
import sonia.scm.PageResult;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.CustomNamespaceStrategy;
+import sonia.scm.repository.ImportHandler;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryHandler;
import sonia.scm.repository.RepositoryInitializer;
import sonia.scm.repository.RepositoryManager;
+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.user.User;
@@ -66,6 +72,7 @@ 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;
@@ -77,7 +84,10 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyObject;
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 +102,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 +114,10 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
@Mock
private RepositoryService service;
@Mock
+ private RepositoryHandler repositoryHandler;
+ @Mock
+ private ImportHandler importHandler;
+ @Mock
private ScmPathInfoStore scmPathInfoStore;
@Mock
private ScmPathInfo uriInfo;
@@ -133,6 +147,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, serviceFactory, resourceLinks);
dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(scmPathInfoStore.get()).thenReturn(uriInfo);
@@ -443,6 +458,51 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
verify(repositoryManager).rename(repository1, "space", "x");
}
+ @Test
+ public void shouldImportRepositoryFromUrl() throws URISyntaxException, IOException {
+ when(manager.getHandler("git")).thenReturn(repositoryHandler);
+ when(repositoryHandler.getImportHandler()).thenReturn(importHandler);
+ when(repositoryHandler.getType()).thenReturn(new RepositoryType("git", "git", ImmutableSet.of(Command.PULL)));
+ when(service.getPullCommand()).thenReturn(mock(PullCommandBuilder.class, RETURNS_SELF));
+
+ 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());
+ }
+
+ @Test
+ public void shouldImportRepositoryFromUrlWithCredentials() throws URISyntaxException, IOException {
+ when(manager.getHandler("git")).thenReturn(repositoryHandler);
+ when(repositoryHandler.getImportHandler()).thenReturn(importHandler);
+ when(repositoryHandler.getType()).thenReturn(new RepositoryType("git", "git", ImmutableSet.of(Command.PULL)));
+ PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF);
+ when(service.getPullCommand()).thenReturn(pullCommandBuilder);
+
+ URL url = Resources.getResource("sonia/scm/api/v2/import-repo-with-credentials.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(pullCommandBuilder).withUsername("trillian");
+ verify(pullCommandBuilder).withPassword("secret");
+ }
+
private PageResult createSingletonPageResult(Repository repository) {
return new PageResult<>(singletonList(repository), 0);
}
diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo-with-credentials.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo-with-credentials.json
new file mode 100644
index 0000000000..bae6d00a1f
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo-with-credentials.json
@@ -0,0 +1,7 @@
+{
+ "namespace": "hitchhiker",
+ "name": "HeartOfGold",
+ "url": "https://scm-manager-org/scm/repo/secret/puzzle42",
+ "username": "trillian",
+ "password": "secret"
+}
diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo.json
new file mode 100644
index 0000000000..af285f1ee7
--- /dev/null
+++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo.json
@@ -0,0 +1,5 @@
+{
+ "namespace": "hitchhiker",
+ "name": "HeartOfGold",
+ "url": "https://scm-manager-org/scm/repo/secret/puzzle42"
+}