diff --git a/CHANGELOG.md b/CHANGELOG.md index 2faf86bbbf..cfdc11e69c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Font ttf-dejavu included oci image ([#1498](https://github.com/scm-manager/scm-manager/issues/1498)) - Repository import and export with metadata for Subversion ([#1501](https://github.com/scm-manager/scm-manager/pull/1501)) - API for store rename/delete in update steps ([#1505](https://github.com/scm-manager/scm-manager/pull/1505)) -- Add import and export for Git via dump file ([#1507](https://github.com/scm-manager/scm-manager/pull/1507)) +- Import and export for Git via dump file ([#1507](https://github.com/scm-manager/scm-manager/pull/1507)) +- Import and export for Mercurial via dump file ([#1511](https://github.com/scm-manager/scm-manager/pull/1511)) ### Changed - Directory name for git LFS files ([#1504](https://github.com/scm-manager/scm-manager/pull/1504)) diff --git a/docs/de/user/repo/assets/import-repository-with-metadata.png b/docs/de/user/repo/assets/import-repository-with-metadata.png deleted file mode 100644 index 43b60b92c8..0000000000 Binary files a/docs/de/user/repo/assets/import-repository-with-metadata.png and /dev/null differ diff --git a/docs/de/user/repo/assets/import-repository.png b/docs/de/user/repo/assets/import-repository.png index 0932484a73..90b02347ea 100644 Binary files a/docs/de/user/repo/assets/import-repository.png and b/docs/de/user/repo/assets/import-repository.png differ diff --git a/docs/de/user/repo/assets/repository-settings-general-git.png b/docs/de/user/repo/assets/repository-settings-general-git.png index b3747418f0..abc9def407 100644 Binary files a/docs/de/user/repo/assets/repository-settings-general-git.png and b/docs/de/user/repo/assets/repository-settings-general-git.png differ diff --git a/docs/de/user/repo/assets/repository-settings-general-svn.png b/docs/de/user/repo/assets/repository-settings-general-svn.png deleted file mode 100644 index d1a7184bc9..0000000000 Binary files a/docs/de/user/repo/assets/repository-settings-general-svn.png and /dev/null differ diff --git a/docs/de/user/repo/index.md b/docs/de/user/repo/index.md index 0f6ae32107..368a7880ff 100644 --- a/docs/de/user/repo/index.md +++ b/docs/de/user/repo/index.md @@ -44,16 +44,11 @@ Neben dem Erstellen von neuen Repository können auch bestehende Repository in S 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. +Zusätzlich zum normalen Repository Import gibt es die Möglichkeit ein Repository Archiv mit Metadaten zu importieren. +Dieses Repository Archiv muss von einem anderen SCM-Manager exportiert worden sein und wird beim Importieren auf Kompatibilität der Daten überprüft. ![Repository importieren](assets/import-repository.png) -Für Subversion Repositories besteht die Möglichkeit, ein Repository inkl. Metadaten zu importieren. -Dabei muss als Quelle ein Repository Archiv ausgewählt werden, welches vorher von einem SCM-Manager exportiert wurde. -Der Import mit Metadaten unterstützt noch keine Migration der Plugin Daten, -deshalb müssen die Versionen des SCM-Managers und die Versionen sämtlicher Plugins zwischen der exportierenden Instanz und der importierenden Instanz exakt übereinstimmen. -Wenn sich die installierten Plugins zwischen diesen beiden Instanzen unterscheiden, sollte dies kein Problem verursachen. -![Repository mit Metadaten importieren](assets/import-repository-with-metadata.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/de/user/repo/settings.md b/docs/de/user/repo/settings.md index 6771f39743..13d6b5ea73 100644 --- a/docs/de/user/repo/settings.md +++ b/docs/de/user/repo/settings.md @@ -17,15 +17,15 @@ umzubenennen, zu löschen oder als archiviert zu markieren. Wenn in der globalen Strategie `benutzerdefiniert` ausgewählt ist, kann zusätzlich zum Repository Namen auch der Namespace umbenannt werden. Ein archiviertes Repository kann nicht mehr verändert werden. +In dem Bereich "Repository exportieren" kann das Repository in unterschiedlichen Formaten exportiert werden. +Das Ausgabeformat des Repository kann über die angebotenen Optionen verändert werden: +* `Standard`: Werden keine Optionen ausgewählt, wird das Repository im Standard Format exportiert. + Git und Mercurial werden dabei als `Tar Archiv` exportiert und Subversion nutzt das `Dump` Format. +* `Komprimieren`: Das Ausgabeformat wird zusätzlich mit `GZip` komprimiert, um die Dateigröße zu verringern. +* `Mit Metadaten`: Statt dem Standard-Format wird ein Repository Archiv exportiert, welches außer dem Repository noch weitere Metadaten enthält. + ![Repository-Settings-General-Git](assets/repository-settings-general-git.png) -In dem Bereich "Repository exportieren" kann das Repository exportiert werden. -Für den Download kann zwischen einem einfachen Dump des reinen Repositories und einem Repository Archiv inkl. der SCM-Manager Metadaten wie Plugin-Konfigurationen oder anderen Daten gewählt werden. -Der Dump kann optional komprimiert werden. Das Repository Archiv mit Metadaten wird immer komprimiert ausgeliefert. -Diese Export-Funktion wird derzeit nur von Subversion Repositories unterstützt. - -![Repository-Settings-General-Svn](assets/repository-settings-general-svn.png) - ### Berechtigungen Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren diff --git a/docs/en/user/repo/assets/import-repository-with-metadata.png b/docs/en/user/repo/assets/import-repository-with-metadata.png deleted file mode 100644 index 2c859aca9a..0000000000 Binary files a/docs/en/user/repo/assets/import-repository-with-metadata.png and /dev/null differ diff --git a/docs/en/user/repo/assets/import-repository.png b/docs/en/user/repo/assets/import-repository.png index 2a518d1302..f2967400db 100644 Binary files a/docs/en/user/repo/assets/import-repository.png and b/docs/en/user/repo/assets/import-repository.png differ diff --git a/docs/en/user/repo/assets/repository-settings-general-git.png b/docs/en/user/repo/assets/repository-settings-general-git.png index 76a78e68b1..018b31f8af 100644 Binary files a/docs/en/user/repo/assets/repository-settings-general-git.png and b/docs/en/user/repo/assets/repository-settings-general-git.png differ diff --git a/docs/en/user/repo/index.md b/docs/en/user/repo/index.md index 6dce67aa9b..a0734243d6 100644 --- a/docs/en/user/repo/index.md +++ b/docs/en/user/repo/index.md @@ -41,7 +41,9 @@ If the namespace strategy is set to custom, the namespace field is also mandator 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. +Your repository will be added to SCM-Manager and all repository data including all branches and tags will be imported. +In addition to the normal repository import, there is the possibility to import a repository archive with metadata. +This repository archive must have been exported from another SCM manager and is checked for data compatibility during import. ![Import Repository](assets/import-repository.png) diff --git a/docs/en/user/repo/settings.md b/docs/en/user/repo/settings.md index fc9d8eaed4..ded1bc664c 100644 --- a/docs/en/user/repo/settings.md +++ b/docs/en/user/repo/settings.md @@ -15,15 +15,15 @@ In the danger zone at the bottom you may rename the repository, delete it or mar strategy in the global SCM-Manager config is set to `custom` you may even rename the repository namespace. If a repository is marked as archived, it can no longer be modified. +In the "Export repository" section the repository can be exported in different formats. +The output format of the repository can be changed via the offered options: +* `Standard`: If no options are selected, the repository will be exported in the standard format. + Git and Mercurial are exported as `Tar archive` and Subversion uses the `Dump` format. +* `Compress`: The output format is additionally compressed with `GZip` to reduce the file size. +* `With metadata`: Instead of the standard format a repository archive is exported, which contains additional metadata besides the repository. + ![Repository-Settings-General-Git](assets/repository-settings-general-git.png) -In the area "Repository Export" you may export this repository. -You can choose to export a simple dump of the repository or a repository archive including all SCM-Manager metadata like plugin configuration or other data. -For a simple repository dump you can choose between compressed or uncompressed file format. The repository archive is always compressed. -This export function is currently only supported by Subversion repositories. - -![Repository-Settings-General-Svn](assets/repository-settings-general-svn.png) - ### Permissions Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable diff --git a/scm-plugins/scm-hg-plugin/build.gradle b/scm-plugins/scm-hg-plugin/build.gradle index 43cbf6d457..b1294f1dec 100644 --- a/scm-plugins/scm-hg-plugin/build.gradle +++ b/scm-plugins/scm-hg-plugin/build.gradle @@ -32,6 +32,7 @@ dependencies { exclude group: 'com.google.guava', module: 'guava' exclude group: 'org.slf4j' } + implementation libraries.commonsCompress testImplementation libraries.shiroUnit testImplementation libraries.logback } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBundleCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBundleCommand.java new file mode 100644 index 0000000000..006101dc48 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBundleCommand.java @@ -0,0 +1,107 @@ +/* + * 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.spi; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import sonia.scm.ContextEntry; +import sonia.scm.repository.api.BundleResponse; +import sonia.scm.repository.api.ExportFailedException; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class HgBundleCommand implements BundleCommand { + + private static final String TAR_ARCHIVE = "tar"; + private final HgCommandContext context; + + + public HgBundleCommand(HgCommandContext context) { + this.context = context; + } + + @Override + public BundleResponse bundle(BundleCommandRequest request) throws IOException { + Path repoDir = context.getDirectory().toPath(); + if (Files.exists(repoDir)) { + try (OutputStream os = request.getArchive().openStream(); + BufferedOutputStream bos = new BufferedOutputStream(os); + TarArchiveOutputStream taos = new TarArchiveOutputStream(bos)) { + + createTarEntryForFiles("", repoDir, taos); + taos.finish(); + } + } else { + throw new ExportFailedException( + ContextEntry.ContextBuilder.noContext(), + "Could not export repository. Repository directory does not exist." + ); + } + return new BundleResponse(0); + } + + @Override + public String getFileExtension() { + return TAR_ARCHIVE; + } + + private void createTarEntryForFiles(String path, Path fileOrDir, TarArchiveOutputStream taos) throws IOException { + try (Stream files = Files.list(fileOrDir)) { + if (files != null) { + files.forEach(f -> bundleFileOrDir(path, f, taos)); + } + } + } + + private void bundleFileOrDir(String path, Path fileOrDir, TarArchiveOutputStream taos) { + try { + String filePath = path + "/" + fileOrDir.getFileName().toString(); + if (Files.isDirectory(fileOrDir)) { + createTarEntryForFiles(filePath, fileOrDir, taos); + } else { + createArchiveEntryForFile(filePath, fileOrDir, taos); + } + } catch (IOException e) { + throw new ExportFailedException( + ContextEntry.ContextBuilder.noContext(), + "Could not export repository. Error on bundling files.", + e + ); + } + } + + private void createArchiveEntryForFile(String filePath, Path path, TarArchiveOutputStream taos) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(filePath); + entry.setSize(path.toFile().length()); + taos.putArchiveEntry(entry); + Files.copy(path, taos); + taos.closeArchiveEntry(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index b70c2b1b69..a766f35fe3 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -56,7 +56,9 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { Command.OUTGOING, Command.PUSH, Command.PULL, - Command.MODIFY + Command.MODIFY, + Command.BUNDLE, + Command.UNBUNDLE ); public static final Set FEATURES = EnumSet.of(Feature.COMBINED_DEFAULT_BRANCH); @@ -267,4 +269,13 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { return new HgTagCommand(context, handler.getWorkingCopyFactory()); } + @Override + public BundleCommand getBundleCommand() { + return new HgBundleCommand(context); + } + + @Override + public UnbundleCommand getUnbundleCommand() { + return new HgUnbundleCommand(context); + } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java new file mode 100644 index 0000000000..99a2bbe891 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java @@ -0,0 +1,82 @@ +/* + * 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.spi; + +import com.google.common.io.ByteSource; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.api.UnbundleResponse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import static com.google.common.base.Preconditions.checkNotNull; + + +public class HgUnbundleCommand implements UnbundleCommand { + private static final Logger LOG = LoggerFactory.getLogger(HgUnbundleCommand.class); + + private final HgCommandContext context; + + HgUnbundleCommand(HgCommandContext context) { + this.context = context; + } + + @Override + public UnbundleResponse unbundle(UnbundleCommandRequest request) throws IOException { + ByteSource archive = checkNotNull(request.getArchive(),"archive is required"); + Path repositoryDir = context.getDirectory().toPath(); + LOG.debug("archive repository {} to {}", repositoryDir, archive); + + if (!Files.exists(repositoryDir)) { + Files.createDirectories(repositoryDir); + } + + unbundleRepositoryFromRequest(request, repositoryDir); + return new UnbundleResponse(0); + } + + private void unbundleRepositoryFromRequest(UnbundleCommandRequest request, Path repositoryDir) throws IOException { + try (TarArchiveInputStream tais = new TarArchiveInputStream(request.getArchive().openBufferedStream())) { + TarArchiveEntry entry; + while ((entry = tais.getNextTarEntry()) != null) { + Path filePath = repositoryDir.resolve(entry.getName()); + createDirectoriesIfNestedFile(filePath); + Files.copy(tais, filePath, StandardCopyOption.REPLACE_EXISTING); + } + } + } + + private void createDirectoriesIfNestedFile(Path filePath) throws IOException { + Path directory = filePath.getParent(); + if (!Files.exists(directory)) { + Files.createDirectories(directory); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBundleCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBundleCommandTest.java new file mode 100644 index 0000000000..e33965159a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBundleCommandTest.java @@ -0,0 +1,105 @@ +/* + * 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.spi; + +import com.google.common.io.ByteSink; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.utils.IOUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class HgBundleCommandTest { + + private HgBundleCommand bundleCommand; + + @Test + void shouldBundleRepository(@TempDir Path temp) throws IOException { + Path repoDir = mockHgContextWithRepoDir(temp); + if (!Files.exists(repoDir)) { + Files.createDirectories(repoDir); + } + String content = "readme testdata"; + addFileToRepoDir(repoDir, "README.md", content); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + BundleCommandRequest bundleCommandRequest = createBundleCommandRequest(baos); + bundleCommand.bundle(bundleCommandRequest); + + assertStreamContainsContent(baos, content); + } + + @Test + void shouldReturnTarForGitBundleCommand() { + bundleCommand = new HgBundleCommand(mock(HgCommandContext.class)); + String fileExtension = bundleCommand.getFileExtension(); + assertThat(fileExtension).isEqualTo("tar"); + } + + private void addFileToRepoDir(Path repoDir, String filename, String content) throws IOException { + Path file = repoDir.resolve(filename); + Files.copy(new ByteArrayInputStream(content.getBytes()), file, StandardCopyOption.REPLACE_EXISTING); + } + + private Path mockHgContextWithRepoDir(Path temp) { + HgCommandContext context = mock(HgCommandContext.class); + bundleCommand = new HgBundleCommand(context); + Path repoDir = Paths.get(temp.toString(), "repository"); + when(context.getDirectory()).thenReturn(repoDir.toFile()); + return repoDir; + } + + private BundleCommandRequest createBundleCommandRequest(ByteArrayOutputStream baos) { + ByteSink byteSink = new ByteSink() { + @Override + public OutputStream openStream() { + return baos; + } + }; + return new BundleCommandRequest(byteSink); + } + + private void assertStreamContainsContent(ByteArrayOutputStream baos, String content) throws IOException { + TarArchiveInputStream tais = new TarArchiveInputStream(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray()))); + tais.getNextEntry(); + + byte[] result = IOUtils.toByteArray(tais); + assertThat(new String(result)).isEqualTo(content); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java new file mode 100644 index 0000000000..6ee26418ac --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java @@ -0,0 +1,103 @@ +/* + * 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.spi; + +import com.google.common.io.ByteSource; +import com.google.common.io.Files; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class HgUnbundleCommandTest { + private HgCommandContext hgContext; + private HgUnbundleCommand unbundleCommand; + + @BeforeEach + void initCommand() { + hgContext = mock(HgCommandContext.class); + unbundleCommand = new HgUnbundleCommand(hgContext); + } + + @Test + void shouldUnbundleRepositoryFiles(@TempDir Path temp) throws IOException { + String filePath = "test-input"; + String fileContent = "HeartOfGold"; + UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(temp, filePath, fileContent); + + unbundleCommand.unbundle(unbundleCommandRequest); + + assertFileWithContentWasCreated(temp, filePath, fileContent); + } + + @Test + void shouldUnbundleNestedRepositoryFiles(@TempDir Path temp) throws IOException { + String filePath = "objects/pack/test-input"; + String fileContent = "hitchhiker"; + UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(temp, filePath, fileContent); + + unbundleCommand.unbundle(unbundleCommandRequest); + + assertFileWithContentWasCreated(temp, filePath, fileContent); + } + + private void assertFileWithContentWasCreated(@TempDir Path temp, String filePath, String fileContent) throws IOException { + File createdFile = temp.resolve(filePath).toFile(); + assertThat(createdFile).exists(); + assertThat(Files.readLines(createdFile, StandardCharsets.UTF_8).get(0)).isEqualTo(fileContent); + } + + private UnbundleCommandRequest createUnbundleCommandRequestForFile(Path temp, String filePath, String fileContent) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + TarArchiveOutputStream taos = new TarArchiveOutputStream(baos); + addEntry(taos, filePath, fileContent); + taos.finish(); + + when(hgContext.getDirectory()).thenReturn(temp.toFile()); + ByteSource byteSource = ByteSource.wrap(baos.toByteArray()); + UnbundleCommandRequest unbundleCommandRequest = new UnbundleCommandRequest(byteSource); + return unbundleCommandRequest; + } + + private void addEntry(TarArchiveOutputStream taos, String name, String input) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(name); + byte[] data = input.getBytes(); + entry.setSize(data.length); + taos.putArchiveEntry(entry); + taos.write(data); + taos.closeArchiveEntry(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java index 4ecde33aa6..c4d897db18 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java @@ -158,15 +158,15 @@ public class RepositoryExportResource { ) ) public Response exportFullRepository(@Context UriInfo uriInfo, - @PathParam("namespace") String namespace, - @PathParam("name") String name, - @Pattern(regexp = "\\w{1,10}") @PathParam("type") String type + @PathParam("namespace") String namespace, + @PathParam("name") String name, + @Pattern(regexp = "\\w{1,10}") @PathParam("type") String type ) { Repository repository = getVerifiedRepository(namespace, name, type); StreamingOutput output = os -> fullScmRepositoryExporter.export(repository, os); return Response - .ok(output, "application/x-gzip") + .ok(output, "application/x-gzip") .header("content-disposition", createContentDispositionHeaderValue(repository, "tar.gz")) .build(); }