mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 15:05:44 +01:00
Merge with default
This commit is contained in:
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Set individual page title
|
- Set individual page title
|
||||||
- Copy on write
|
- Copy on write
|
||||||
- A new repository can be initialized with a branch (for git and mercurial) and custom files (README.md on default)
|
- A new repository can be initialized with a branch (for git and mercurial) and custom files (README.md on default)
|
||||||
|
- Plugins are validated directly after download
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Stop fetching commits when it takes too long
|
- Stop fetching commits when it takes too long
|
||||||
@@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Subversion revision 0 leads to error
|
- Subversion revision 0 leads to error
|
||||||
- Create mock subject to satisfy legman
|
- Create mock subject to satisfy legman
|
||||||
- Multiple versions of hibernate-validator caused problems when starting from plugins
|
- Multiple versions of hibernate-validator caused problems when starting from plugins
|
||||||
|
- Page title is now set correctly
|
||||||
|
|
||||||
## 2.0.0-rc1 - 2019-12-02
|
## 2.0.0-rc1 - 2019-12-02
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.tmatesoft.svn.core.SVNCommitInfo;
|
import org.tmatesoft.svn.core.SVNCommitInfo;
|
||||||
import org.tmatesoft.svn.core.SVNDepth;
|
import org.tmatesoft.svn.core.SVNDepth;
|
||||||
import org.tmatesoft.svn.core.SVNException;
|
import org.tmatesoft.svn.core.SVNException;
|
||||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||||
import org.tmatesoft.svn.core.wc.SVNWCClient;
|
import org.tmatesoft.svn.core.wc.SVNWCClient;
|
||||||
|
import org.tmatesoft.svn.core.wc.SVNWCUtil;
|
||||||
import sonia.scm.repository.InternalRepositoryException;
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.SvnWorkDirFactory;
|
import sonia.scm.repository.SvnWorkDirFactory;
|
||||||
@@ -38,6 +40,7 @@ public class SvnModifyCommand implements ModifyCommand {
|
|||||||
|
|
||||||
private String commitChanges(SVNClientManager clientManager, File workingDirectory, String commitMessage) {
|
private String commitChanges(SVNClientManager clientManager, File workingDirectory, String commitMessage) {
|
||||||
try {
|
try {
|
||||||
|
clientManager.setAuthenticationManager(SVNWCUtil.createDefaultAuthenticationManager(getCurrentUserName(), new char[0]));
|
||||||
SVNCommitInfo svnCommitInfo = clientManager.getCommitClient().doCommit(
|
SVNCommitInfo svnCommitInfo = clientManager.getCommitClient().doCommit(
|
||||||
new File[]{workingDirectory},
|
new File[]{workingDirectory},
|
||||||
false,
|
false,
|
||||||
@@ -54,6 +57,14 @@ public class SvnModifyCommand implements ModifyCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getCurrentUserName() {
|
||||||
|
if (SecurityUtils.getSubject() != null && SecurityUtils.getSubject().getPrincipal() != null) {
|
||||||
|
return SecurityUtils.getSubject().getPrincipal().toString();
|
||||||
|
} else {
|
||||||
|
return "SCM-Manager";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void modifyWorkingDirectory(ModifyCommandRequest request, SVNClientManager clientManager, File workingDirectory) {
|
private void modifyWorkingDirectory(ModifyCommandRequest request, SVNClientManager clientManager, File workingDirectory) {
|
||||||
for (ModifyCommandRequest.PartialRequest partialRequest : request.getRequests()) {
|
for (ModifyCommandRequest.PartialRequest partialRequest : request.getRequests()) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.apache.shiro.util.ThreadContext;
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -14,6 +17,8 @@ import java.io.IOException;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class SvnModifyCommandTest extends AbstractSvnCommandTestBase {
|
public class SvnModifyCommandTest extends AbstractSvnCommandTestBase {
|
||||||
|
|
||||||
@@ -31,6 +36,18 @@ public class SvnModifyCommandTest extends AbstractSvnCommandTestBase {
|
|||||||
svnModifyCommand = new SvnModifyCommand(context, createRepository(), workDirFactory);
|
svnModifyCommand = new SvnModifyCommand(context, createRepository(), workDirFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initSecurityManager() {
|
||||||
|
Subject subject = mock(Subject.class);
|
||||||
|
when(subject.getPrincipal()).thenReturn("alThor");
|
||||||
|
ThreadContext.bind(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void cleanUpSecurityManager() {
|
||||||
|
ThreadContext.unbindSubject();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldRemoveFiles() {
|
public void shouldRemoveFiles() {
|
||||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ const PageActionContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default class Page extends React.Component<Props> {
|
export default class Page extends React.Component<Props> {
|
||||||
|
componentDidUpdate() {
|
||||||
|
const { title } = this.props;
|
||||||
|
if (title && title !== document.title) {
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { error } = this.props;
|
const { error } = this.props;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ class PluginInstaller {
|
|||||||
|
|
||||||
private final SCMContextProvider context;
|
private final SCMContextProvider context;
|
||||||
private final AdvancedHttpClient client;
|
private final AdvancedHttpClient client;
|
||||||
|
private final SmpDescriptorExtractor smpDescriptorExtractor;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client) {
|
public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
this.smpDescriptorExtractor = smpDescriptorExtractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("squid:S4790") // hashing should be safe
|
@SuppressWarnings("squid:S4790") // hashing should be safe
|
||||||
@@ -34,6 +36,7 @@ class PluginInstaller {
|
|||||||
Files.copy(input, file);
|
Files.copy(input, file);
|
||||||
|
|
||||||
verifyChecksum(plugin, input.hash(), file);
|
verifyChecksum(plugin, input.hash(), file);
|
||||||
|
verifyConditions(plugin, file);
|
||||||
return new PendingPluginInstallation(plugin.install(), file);
|
return new PendingPluginInstallation(plugin.install(), file);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
cleanup(file);
|
cleanup(file);
|
||||||
@@ -64,6 +67,20 @@ class PluginInstaller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void verifyConditions(AvailablePlugin plugin, Path file) throws IOException {
|
||||||
|
InstalledPluginDescriptor pluginDescriptor = smpDescriptorExtractor.extractPluginDescriptor(file);
|
||||||
|
if (!pluginDescriptor.getCondition().isSupported()) {
|
||||||
|
cleanup(file);
|
||||||
|
throw new PluginConditionFailedException(
|
||||||
|
pluginDescriptor.getCondition(),
|
||||||
|
String.format(
|
||||||
|
"could not load plugin %s, the plugin condition does not match",
|
||||||
|
plugin.getDescriptor().getInformation().getName()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private InputStream download(AvailablePlugin plugin) throws IOException {
|
private InputStream download(AvailablePlugin plugin) throws IOException {
|
||||||
return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream();
|
return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXBContext;
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
class SmpDescriptorExtractor {
|
||||||
|
|
||||||
|
InstalledPluginDescriptor extractPluginDescriptor(Path file) throws IOException {
|
||||||
|
try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8)) {
|
||||||
|
ZipEntry nextEntry;
|
||||||
|
while ((nextEntry = zipInputStream.getNextEntry()) != null) {
|
||||||
|
if ("META-INF/scm/plugin.xml".equals(nextEntry.getName())) {
|
||||||
|
JAXBContext context = JAXBContext.newInstance(ScmModule.class, InstalledPluginDescriptor.class);
|
||||||
|
return (InstalledPluginDescriptor) context.createUnmarshaller().unmarshal(zipInputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (JAXBException e) {
|
||||||
|
throw new IOException("failed to read descriptor file META-INF/scm/plugin.xml from plugin", e);
|
||||||
|
}
|
||||||
|
throw new IOException("Missing plugin descriptor META-INF/scm/plugin.xml in download package");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,18 +32,23 @@ class PluginInstallerTest {
|
|||||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
private AdvancedHttpClient client;
|
private AdvancedHttpClient client;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SmpDescriptorExtractor extractor;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private PluginInstaller installer;
|
private PluginInstaller installer;
|
||||||
|
|
||||||
private Path directory;
|
private Path directory;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUpContext(@TempDirectory.TempDir Path directory) {
|
void setUpContext(@TempDirectory.TempDir Path directory) throws IOException {
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
lenient().when(context.resolve(any())).then(ic -> {
|
lenient().when(context.resolve(any())).then(ic -> {
|
||||||
Path arg = ic.getArgument(0);
|
Path arg = ic.getArgument(0);
|
||||||
return directory.resolve(arg);
|
return directory.resolve(arg);
|
||||||
});
|
});
|
||||||
|
InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(true);
|
||||||
|
lenient().when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -105,6 +110,15 @@ class PluginInstallerTest {
|
|||||||
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
|
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailForUnsupportedPlugin() throws IOException {
|
||||||
|
mockContent("42");
|
||||||
|
InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(false);
|
||||||
|
when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin);
|
||||||
|
|
||||||
|
assertThrows(PluginConditionFailedException.class, () -> installer.install(createGitPlugin()));
|
||||||
|
assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist();
|
||||||
|
}
|
||||||
|
|
||||||
private AvailablePlugin createPlugin(String name, String url, String checksum) {
|
private AvailablePlugin createPlugin(String name, String url, String checksum) {
|
||||||
PluginInformation information = new PluginInformation();
|
PluginInformation information = new PluginInformation();
|
||||||
@@ -114,4 +128,11 @@ class PluginInstallerTest {
|
|||||||
);
|
);
|
||||||
return new AvailablePlugin(descriptor);
|
return new AvailablePlugin(descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private InstalledPluginDescriptor createPluginDescriptor(boolean supported) {
|
||||||
|
InstalledPluginDescriptor installedPluginDescriptor = mock(InstalledPluginDescriptor.class, RETURNS_DEEP_STUBS);
|
||||||
|
lenient().when(installedPluginDescriptor.getCondition().isSupported()).thenReturn(supported);
|
||||||
|
lenient().when(installedPluginDescriptor.getInformation().getId()).thenReturn("scm-git-plugin");
|
||||||
|
return installedPluginDescriptor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class SmpDescriptorExtractorTest {
|
||||||
|
|
||||||
|
private static final String PLUGIN_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
|
||||||
|
"<plugin>\n" +
|
||||||
|
"\n" +
|
||||||
|
" <scm-version>2</scm-version>\n" +
|
||||||
|
"\n" +
|
||||||
|
" <information>\n" +
|
||||||
|
" <displayName>Test</displayName>\n" +
|
||||||
|
" <author>Cloudogu GmbH</author>\n" +
|
||||||
|
" <category>Testing</category>\n" +
|
||||||
|
" <name>scm-test-plugin</name>\n" +
|
||||||
|
"<version>2.0.0</version>\n" +
|
||||||
|
"<description>Collects information for support cases</description>\n" +
|
||||||
|
"</information>\n" +
|
||||||
|
"\n" +
|
||||||
|
" <conditions>\n" +
|
||||||
|
" <min-version>2.0.0-rc1</min-version>\n" +
|
||||||
|
" </conditions>\n" +
|
||||||
|
"\n" +
|
||||||
|
" <resources>\n" +
|
||||||
|
" <script>assets/scm-support-plugin.bundle.js</script>\n" +
|
||||||
|
" </resources>\n" +
|
||||||
|
"\n" +
|
||||||
|
"</plugin>\n";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExtractPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", PLUGIN_XML);
|
||||||
|
|
||||||
|
InstalledPluginDescriptor installedPluginDescriptor = new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile);
|
||||||
|
|
||||||
|
Assertions.assertThat(installedPluginDescriptor.getInformation().getName()).isEqualTo("scm-test-plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailWithoutPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path pluginFile = createZipFile(tempDir, "META-INF/wrong/plugin.xml", PLUGIN_XML);
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailWithIllegalPluginXml(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", "<not><parsable>content</parsable></not>");
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
Path createZipFile(Path tempDir, String internalFileName, String content) throws IOException {
|
||||||
|
Path pluginFile = tempDir.resolve("scm-test-plugin.smp");
|
||||||
|
ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(pluginFile), UTF_8);
|
||||||
|
zipOutputStream.putNextEntry(new ZipEntry(internalFileName));
|
||||||
|
zipOutputStream.write(content.getBytes(UTF_8));
|
||||||
|
zipOutputStream.closeEntry();
|
||||||
|
zipOutputStream.close();
|
||||||
|
return pluginFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user