Bundle and unbundle command for mercurial (#1511)

Support for exporting and importing mercurial repositories as tar ball

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-01-28 13:09:47 +01:00
committed by GitHub
parent bd3671b428
commit c3ab6bc5d5
19 changed files with 435 additions and 28 deletions

View File

@@ -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))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -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.

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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<Path> 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();
}
}

View File

@@ -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<Feature> 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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}