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" +}