Allow filter for Git repositories

The line `allowfilter = true` is inserted both in new Git repositories
and existing ones (via an UpdateStep). This enables clones with
`--filter` parameters.

Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
This commit is contained in:
Till-André Diegeler
2025-01-09 15:27:22 +01:00
parent a4e30b94a2
commit 08e57b9a19
17 changed files with 438 additions and 35 deletions

View File

@@ -25,10 +25,11 @@ public class GitConfigHelper {
private static final String CONFIG_SECTION_SCMM = "scmm";
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
public void createScmmConfig(Repository repository, org.eclipse.jgit.lib.Repository gitRepository) throws IOException {
StoredConfig config = gitRepository.getConfig();
config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId());
config.save();
public void createScmmConfig(Repository scmmRepository, org.eclipse.jgit.lib.Repository gitRepository) throws IOException {
StoredConfig gitConfig = gitRepository.getConfig();
gitConfig.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, scmmRepository.getId());
gitConfig.setBoolean("uploadpack", null, "allowFilter", true);
gitConfig.save();
}
public String getRepositoryId(StoredConfig gitConfig) {

View File

@@ -30,13 +30,13 @@ import sonia.scm.plugin.Extension;
*/
@Extension
@EagerSingleton
public class GitRepositoryModifyListener {
public class GitRepositoryConfigChangeListener {
private final GitHeadModifier headModifier;
private final GitRepositoryConfigStoreProvider storeProvider;
@Inject
public GitRepositoryModifyListener(GitHeadModifier headModifier, GitRepositoryConfigStoreProvider storeProvider) {
public GitRepositoryConfigChangeListener(GitHeadModifier headModifier, GitRepositoryConfigStoreProvider storeProvider) {
this.headModifier = headModifier;
this.storeProvider = storeProvider;
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.update;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class GitUpdateStepHelper {
private GitUpdateStepHelper() {}
static Path determineEffectiveGitFolder(Path path) {
Path bareGitFolder = path.resolve("data");
Path nonBareGitFolder = bareGitFolder.resolve(".git");
final Path effectiveGitPath;
if (Files.exists(nonBareGitFolder)) {
effectiveGitPath = nonBareGitFolder;
} else {
effectiveGitPath = bareGitFolder;
}
return effectiveGitPath;
}
static org.eclipse.jgit.lib.Repository build(File directory) throws IOException {
return new FileRepositoryBuilder().setGitDir(directory).readEnvironment().findGitDir().build();
}
static boolean isGitDirectory(Repository repository) {
return GitRepositoryHandler.TYPE_NAME.equals(repository.getType());
}
}

View File

@@ -17,22 +17,21 @@
package sonia.scm.repository.update;
import jakarta.inject.Inject;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import sonia.scm.migration.UpdateException;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitConfigHelper;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
import sonia.scm.version.Version;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static sonia.scm.repository.update.GitUpdateStepHelper.build;
import static sonia.scm.repository.update.GitUpdateStepHelper.determineEffectiveGitFolder;
import static sonia.scm.repository.update.GitUpdateStepHelper.isGitDirectory;
import static sonia.scm.version.Version.parse;
@Extension
@@ -64,30 +63,6 @@ public class GitV2UpdateStep implements UpdateStep {
);
}
public Path determineEffectiveGitFolder(Path path) {
Path bareGitFolder = path.resolve("data");
Path nonBareGitFolder = bareGitFolder.resolve(".git");
final Path effectiveGitPath;
if (Files.exists(nonBareGitFolder)) {
effectiveGitPath = nonBareGitFolder;
} else {
effectiveGitPath = bareGitFolder;
}
return effectiveGitPath;
}
private org.eclipse.jgit.lib.Repository build(File directory) throws IOException {
return new FileRepositoryBuilder()
.setGitDir(directory)
.readEnvironment()
.findGitDir()
.build();
}
private boolean isGitDirectory(Repository repository) {
return GitRepositoryHandler.TYPE_NAME.equals(repository.getType());
}
@Override
public Version getTargetVersion() {
return parse("2.0.0");

View File

@@ -0,0 +1,72 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.update;
import jakarta.inject.Inject;
import sonia.scm.migration.RepositoryUpdateContext;
import sonia.scm.migration.RepositoryUpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitConfigHelper;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
import sonia.scm.version.Version;
import java.io.File;
import java.nio.file.Path;
import static sonia.scm.repository.update.GitUpdateStepHelper.build;
import static sonia.scm.repository.update.GitUpdateStepHelper.determineEffectiveGitFolder;
import static sonia.scm.repository.update.GitUpdateStepHelper.isGitDirectory;
import static sonia.scm.version.Version.parse;
@Extension
public class UpdatePackFilterUpdateStep implements RepositoryUpdateStep {
private final RepositoryLocationResolver locationResolver;
private final UpdateStepRepositoryMetadataAccess<Path> repositoryMetadataAccess;
@Inject
public UpdatePackFilterUpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess<Path> repositoryMetadataAccess) {
this.locationResolver = locationResolver;
this.repositoryMetadataAccess = repositoryMetadataAccess;
}
@Override
public void doUpdate(RepositoryUpdateContext repositoryUpdateContext) throws Exception {
Path scmmRepositoryLocation = locationResolver.forClass(Path.class).getLocation(repositoryUpdateContext.getRepositoryId());
Repository scmmRepository = repositoryMetadataAccess.read(scmmRepositoryLocation);
if (isGitDirectory(scmmRepository)) {
File gitFile = determineEffectiveGitFolder(scmmRepositoryLocation).toFile();
org.eclipse.jgit.lib.Repository gitRepository = build(gitFile);
GitConfigHelper gitConfigHelper = new GitConfigHelper();
gitConfigHelper.createScmmConfig(scmmRepository, gitRepository);
}
}
@Override
public Version getTargetVersion() {
return parse("3.7.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.plugin.git";
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
@ExtendWith(MockitoExtension.class)
class GitConfigHelperTest {
@Mock
Repository gitRepository;
StoredConfig gitConfig;
sonia.scm.repository.Repository scmmRepository;
@BeforeEach
void setUp() {
gitConfig = new StoredConfig() {
@Override
public void load() {
// not needed
}
@Override
public void save() {
// not needed
}
};
doReturn(gitConfig).when(gitRepository).getConfig();
scmmRepository = RepositoryTestData.createHeartOfGold();
}
@Test
void shouldSetCorrectScmmRepositoryId() throws IOException {
GitConfigHelper target = new GitConfigHelper();
target.createScmmConfig(scmmRepository, gitRepository);
assertThat(gitConfig.getString("scmm", null, "repositoryid")).isEqualTo(scmmRepository.getId());
}
@Test
void shouldAllowUploadpackFilter() throws IOException {
GitConfigHelper target = new GitConfigHelper();
target.createScmmConfig(scmmRepository, gitRepository);
assertThat(gitConfig.getBoolean("uploadpack", "allowFilter", false)).isTrue();
}
}

View File

@@ -23,8 +23,12 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.schedule.Scheduler;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.InMemoryByteConfigurationStoreFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@@ -79,7 +83,6 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
GitConfig config = new GitConfig();
// TODO fix event bus exception
repositoryHandler.setConfig(config);
return repositoryHandler;
@@ -116,4 +119,28 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
assertThat(new File(nativeRepoDirectory, "HEAD")).hasContent("ref: refs/heads/other");
}
@Test
public void shouldSetAllowFilterConfigByDefault() throws Exception{
ConfigurationStoreFactory configurationStoreFactory = new InMemoryByteConfigurationStoreFactory();
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(configurationStoreFactory,
scheduler, repositoryLocationResolver, gitWorkingCopyFactory,null);
GitConfig config = new GitConfig();
repositoryHandler.setConfig(config);
repositoryHandler.create(RepositoryTestData.createHeartOfGold("git"));
Path repositoryPath = repositoryLocationResolver.forClass(Path.class).getLocation("");
File configFile = repositoryPath.resolve("data/config").toFile();
boolean containsAllowFilter = false;
try (BufferedReader br = new BufferedReader(new FileReader(configFile.getAbsolutePath()))) {
do {
String line = br.readLine();
containsAllowFilter |= line.contains("allowFilter") && line.contains("true");
} while (br.readLine() != null);
}
assertTrue(containsAllowFilter);
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.update;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.TempDirRepositoryLocationResolver;
import sonia.scm.migration.RepositoryUpdateContext;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.io.CleanupMode.ALWAYS;
@ExtendWith(MockitoExtension.class)
class UpdatePackFilterUpdateStepTest {
RepositoryLocationResolver repositoryLocationResolver;
UpdateStepRepositoryMetadataAccess<Path> updateStepRepositoryMetadataAccess;
@Nested
class DoUpdate {
@TempDir(cleanup = ALWAYS)
Path tempDir;
UpdatePackFilterUpdateStep target;
private static final String EXAMPLE_REPOSITORY_ID = "3ZUZMNJn3E";
@BeforeEach
void setUp() throws IOException {
String sourcePath = "src/test/resources/scm-home/repositories/exampleGitRepoWithoutFilterUpdate";
loadFilesIntoTempDir(tempDir, sourcePath);
repositoryLocationResolver = new TempDirRepositoryLocationResolver(tempDir.toFile());
updateStepRepositoryMetadataAccess = location -> {
Repository repository = RepositoryTestData.createRestaurantAtTheEndOfTheUniverse("git");
repository.setId(EXAMPLE_REPOSITORY_ID);
return repository;
};
}
@Test
void shouldWriteAllowFilterLineWithinConfig() throws Exception {
target = new UpdatePackFilterUpdateStep(repositoryLocationResolver, updateStepRepositoryMetadataAccess);
target.doUpdate(new RepositoryUpdateContext(EXAMPLE_REPOSITORY_ID));
File configFile = tempDir.resolve("data/config").toFile();
boolean containsAllowFilter = false;
try (BufferedReader br = new BufferedReader(new FileReader(configFile.getAbsolutePath()))) {
do {
String line = br.readLine();
containsAllowFilter |= line.contains("allowFilter") && line.contains("true");
} while (br.readLine() != null);
}
assertTrue(containsAllowFilter);
}
}
private void loadFilesIntoTempDir(Path tempDir, String sourcePath) throws IOException {
File repositorySourcePath = new File(sourcePath);
try (Stream<Path> sources = Files.walk(repositorySourcePath.toPath())) {
sources.forEach(source -> {
Path destination = Paths.get(tempDir.toString(), source.toString().substring(repositorySourcePath.toString().length()));
if (!destination.toFile().exists()) {
try {
Files.copy(source, destination);
} catch (IOException e) {
fail("An exception occurred during temporary repository file setup.", e);
}
}
});
}
}
}

View File

@@ -0,0 +1,7 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true
logallrefupdates = false
[scmm]
repositoryid = 3ZUZMNJn3E

View File

@@ -0,0 +1 @@
43932c31dda188da38d96682d50df14a9407579e

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Copyright (c) 2020 - present Cloudogu GmbH
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see https://www.gnu.org/licenses/.
-->
<repositories>
<properties/>
<id>3ZUZMNJn3E</id>
<namespace>test</namespace>
<name>exampleGitRepoWithoutFilterUpdate</name>
<type>git</type>
<description></description>
<contact></contact>
<creationDate>1736402353654</creationDate>
<lastModified>1736402353753</lastModified>
<permission>
<groupPermission>false</groupPermission>
<name>scmadmin</name>
<role>OWNER</role>
</permission>
<archived>false</archived>
</repositories>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" ?>
<!--
Copyright (c) 2020 - present Cloudogu GmbH
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see https://www.gnu.org/licenses/.
-->
<configuration type="config-entry">
<entry>
<key>sonia.scm.dao.xml</key>
<value>
<latestVersion>2.0.0</latestVersion>
</value>
</entry>
</configuration>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" ?>
<!--
Copyright (c) 2020 - present Cloudogu GmbH
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see https://www.gnu.org/licenses/.
-->
<config>
<defaultBranch>main</defaultBranch>
<nonFastForwardDisallowed>false</nonFastForwardDisallowed>
</config>