From 230ac848ebd72ff476d925a7b03fbf0d4b447580 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 20 Aug 2019 16:39:50 +0200 Subject: [PATCH 01/35] ignore old plugins folder --- .../src/main/java/sonia/scm/plugin/PluginProcessor.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java index b91ee9b1ee..0613f4ef07 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -60,6 +60,8 @@ import java.util.Date; import java.util.List; import java.util.Set; +import static java.util.stream.Collectors.toList; + //~--- JDK imports ------------------------------------------------------------ /** @@ -171,7 +173,11 @@ public final class PluginProcessor extract(archives); - List dirs = collectPluginDirectories(pluginDirectory); + List dirs = + collectPluginDirectories(pluginDirectory) + .stream() + .filter(dir -> !dir.endsWith("sonia.scm.plugins")) + .collect(toList()); logger.debug("process {} directories: {}", dirs.size(), dirs); From 72fe69b2d55ef8b177344404026719cabb6482d7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 21 Aug 2019 10:34:40 +0200 Subject: [PATCH 02/35] add new IllegalIdentifierChangeException --- .../scm/IllegalIdentifierChangeException.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java diff --git a/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java b/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java new file mode 100644 index 0000000000..63b9d0d4fc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/IllegalIdentifierChangeException.java @@ -0,0 +1,21 @@ +package sonia.scm; + +import java.util.Collections; + +public class IllegalIdentifierChangeException extends BadRequestException { + + private static final String CODE = "thbsUFokjk"; + + public IllegalIdentifierChangeException(ContextEntry.ContextBuilder context, String message) { + super(context.build(), message); + } + + public IllegalIdentifierChangeException(String message) { + super(Collections.emptyList(), message); + } + + @Override + public String getCode() { + return CODE; + } +} From 7a29bba3398c52cd0ed0947edb1ef185e78a6d54 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 21 Aug 2019 10:36:48 +0200 Subject: [PATCH 03/35] use new IllegalIdentifierChangeException in SingleResourceManagerAdapter --- .../scm/api/v2/resources/SingleResourceManagerAdapter.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index a7b9146d00..76371a0b54 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; import sonia.scm.ConcurrentModificationException; +import sonia.scm.IllegalIdentifierChangeException; import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.NotFoundException; @@ -11,8 +12,6 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; - /** * Adapter from resource http endpoints to managers, for Single resources (e.g. {@code /user/name}). * @@ -55,7 +54,7 @@ class SingleResourceManagerAdapter Date: Tue, 27 Aug 2019 08:52:38 +0200 Subject: [PATCH 04/35] pluginprocessor only consider directories that contains plugin.xml --- .../src/main/java/sonia/scm/plugin/PluginProcessor.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java index 0613f4ef07..6f1b18034e 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -59,6 +59,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import static java.util.stream.Collectors.toList; @@ -176,7 +177,7 @@ public final class PluginProcessor List dirs = collectPluginDirectories(pluginDirectory) .stream() - .filter(dir -> !dir.endsWith("sonia.scm.plugins")) + .filter(isPluginDirectory()) .collect(toList()); logger.debug("process {} directories: {}", dirs.size(), dirs); @@ -200,6 +201,10 @@ public final class PluginProcessor return ImmutableSet.copyOf(wrappers); } + private Predicate isPluginDirectory() { + return dir -> new File(dir.resolve("META-INF/scm/plugin.xml").toString()).exists(); + } + /** * Method description * From 840b44af378c194e613e0bafcfb0404fd4b837ef Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 27 Aug 2019 11:41:36 +0000 Subject: [PATCH 05/35] Close branch bugfix/redirect_collection_links From b36c2dd698f14d5e951e41870ca2dada2c6d8300 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 27 Aug 2019 13:59:14 +0200 Subject: [PATCH 06/35] use nio api and added test for plugin directory check --- .../main/java/sonia/scm/plugin/PluginProcessor.java | 3 ++- .../java/sonia/scm/plugin/PluginProcessorTest.java | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java index 6f1b18034e..5588349cc8 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -71,6 +71,7 @@ import static java.util.stream.Collectors.toList; * * TODO don't mix nio and io */ +@SuppressWarnings("squid:S3725") // performance is not critical, for this type of checks public final class PluginProcessor { @@ -202,7 +203,7 @@ public final class PluginProcessor } private Predicate isPluginDirectory() { - return dir -> new File(dir.resolve("META-INF/scm/plugin.xml").toString()).exists(); + return dir -> Files.exists(dir.resolve(DIRECTORY_METAINF).resolve("scm").resolve("plugin.xml")); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java index 87e9cbf7b7..e9eaea3afa 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java @@ -134,6 +134,16 @@ public class PluginProcessorTest assertThat(plugin.getId(), is(PLUGIN_A.id)); } + @Test + public void shouldCollectPluginsAndDoNotFailOnNonPluginDirectories() throws IOException { + new File(pluginDirectory, "some-directory").mkdirs(); + + copySmp(PLUGIN_A); + PluginWrapper plugin = collectAndGetFirst(); + + assertThat(plugin.getId(), is(PLUGIN_A.id)); + } + /** * Method description * From 3cd1cdfa4f6bf3efd002e40a31d8b7da4d9987eb Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 27 Aug 2019 12:30:25 +0000 Subject: [PATCH 07/35] Close branch bugfix/ignore_old_plugin_dir From c42b3cca0c281673e1804342110c83eb00ccc396 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 27 Aug 2019 13:16:39 +0000 Subject: [PATCH 08/35] Close branch bugfix/ci_status_validation From 79037afdf8a6ba91b070801b25eec0b5379fbdb8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 27 Aug 2019 15:20:38 +0200 Subject: [PATCH 09/35] update maven-deploy-plugin from 2.7 to 2.8.2 --- pom.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3166c6ec75..3ab3cb82a3 100644 --- a/pom.xml +++ b/pom.xml @@ -439,6 +439,13 @@ smp-maven-plugin 1.0.0-alpha-6 + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + @@ -633,7 +640,6 @@ org.apache.maven.plugins maven-deploy-plugin - 2.7 From 769207c2c15af17b6a894333adf439700bdbd99b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 27 Aug 2019 15:33:30 +0200 Subject: [PATCH 10/35] fixed compilation error --- .../src/test/java/sonia/scm/plugin/PluginProcessorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java index dc5a9add68..bb518ec731 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java @@ -139,7 +139,7 @@ public class PluginProcessorTest new File(pluginDirectory, "some-directory").mkdirs(); copySmp(PLUGIN_A); - PluginWrapper plugin = collectAndGetFirst(); + InstalledPlugin plugin = collectAndGetFirst(); assertThat(plugin.getId(), is(PLUGIN_A.id)); } From 4f21756d007c40f23cbed0f1c1f020154385adf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 27 Aug 2019 16:56:44 +0200 Subject: [PATCH 11/35] Extract class to provide temporary work directories --- .../repository/util/SimpleWorkdirFactory.java | 20 +++---------- .../scm/repository/util/WorkdirProvider.java | 29 +++++++++++++++++++ .../util/SimpleWorkdirFactoryTest.java | 3 +- .../spi/SimpleGitWorkdirFactory.java | 8 ++--- .../repository/spi/GitBranchCommandTest.java | 5 ++-- .../repository/spi/GitMergeCommandTest.java | 3 +- .../spi/SimpleGitWorkdirFactoryTest.java | 11 ++++--- .../spi/SimpleHgWorkdirFactory.java | 4 ++- .../repository/spi/HgBranchCommandTest.java | 3 +- 9 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java diff --git a/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java index 0872242612..7236e0c3fe 100644 --- a/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java @@ -7,29 +7,21 @@ import sonia.scm.repository.Repository; import java.io.File; import java.io.IOException; -import java.nio.file.Files; public abstract class SimpleWorkdirFactory implements WorkdirFactory { private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class); - private final File poolDirectory; + private final WorkdirProvider workdirProvider; - public SimpleWorkdirFactory() { - this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work")); - } - - public SimpleWorkdirFactory(File poolDirectory) { - this.poolDirectory = poolDirectory; - if (!poolDirectory.exists() && !poolDirectory.mkdirs()) { - throw new IllegalStateException("could not create pool directory " + poolDirectory); - } + public SimpleWorkdirFactory(WorkdirProvider workdirProvider) { + this.workdirProvider = workdirProvider; } @Override public WorkingCopy createWorkingCopy(C context) { try { - File directory = createNewWorkdir(); + File directory = workdirProvider.createNewWorkdir(); ParentAndClone parentAndClone = cloneRepository(context, directory); return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::close, directory); } catch (IOException e) { @@ -45,10 +37,6 @@ public abstract class SimpleWorkdirFactory implements WorkdirFactory protected abstract ParentAndClone cloneRepository(C context, File target) throws IOException; - private File createNewWorkdir() throws IOException { - return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile(); - } - private void close(R repository) { try { closeRepository(repository); diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java new file mode 100644 index 0000000000..35dae56faa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirProvider.java @@ -0,0 +1,29 @@ +package sonia.scm.repository.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class WorkdirProvider { + + private final File poolDirectory; + + public WorkdirProvider() { + this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work")); + } + + public WorkdirProvider(File poolDirectory) { + this.poolDirectory = poolDirectory; + if (!poolDirectory.exists() && !poolDirectory.mkdirs()) { + throw new IllegalStateException("could not create pool directory " + poolDirectory); + } + } + + public File createNewWorkdir() { + try { + return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile(); + } catch (IOException e) { + throw new RuntimeException("could not create temporary workdir", e); + } + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java b/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java index 2d3c2ed59e..31aa11c604 100644 --- a/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java @@ -28,7 +28,8 @@ public class SimpleWorkdirFactoryTest { @Before public void initFactory() throws IOException { - simpleWorkdirFactory = new SimpleWorkdirFactory(temporaryFolder.newFolder()) { + WorkdirProvider workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); + simpleWorkdirFactory = new SimpleWorkdirFactory(workdirProvider) { @Override protected Repository getScmRepository(Context context) { return REPOSITORY; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java index c30ff59afb..28042f0834 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java @@ -7,16 +7,14 @@ import org.eclipse.jgit.transport.ScmTransportProtocol; import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.util.SimpleWorkdirFactory; +import sonia.scm.repository.util.WorkdirProvider; import java.io.File; public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory implements GitWorkdirFactory { - public SimpleGitWorkdirFactory() { - } - - SimpleGitWorkdirFactory(File poolDirectory) { - super(poolDirectory); + public SimpleGitWorkdirFactory(WorkdirProvider workdirProvider) { + super(workdirProvider); } @Override diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java index e429aa5a42..aa5e641b31 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java @@ -5,6 +5,7 @@ import org.junit.Rule; import org.junit.Test; import sonia.scm.repository.Branch; import sonia.scm.repository.api.BranchRequest; +import sonia.scm.repository.util.WorkdirProvider; import java.io.IOException; import java.util.List; @@ -25,7 +26,7 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase { branchRequest.setParentBranch(source.getName()); branchRequest.setNewBranch("new_branch"); - new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory()).branch(branchRequest); + new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest); Branch newBranch = findBranch(context, "new_branch"); Assertions.assertThat(newBranch.getRevision()).isEqualTo(source.getRevision()); @@ -45,7 +46,7 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase { BranchRequest branchRequest = new BranchRequest(); branchRequest.setNewBranch("new_branch"); - new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory()).branch(branchRequest); + new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest); Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index e8a62f5b86..8b3b4548d9 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -24,6 +24,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; +import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.user.User; import java.io.IOException; @@ -244,6 +245,6 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { } private GitMergeCommand createCommand() { - return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory()); + return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java index 616df5e68e..1b3c730ef1 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java @@ -11,6 +11,7 @@ import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.repository.util.WorkingCopy; import java.io.File; @@ -29,19 +30,21 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { // keep this so that it will not be garbage collected (Transport keeps this in a week reference) private ScmTransportProtocol proto; + private WorkdirProvider workdirProvider; @Before - public void bindScmProtocol() { + public void bindScmProtocol() throws IOException { HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory); GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); proto = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)); Transport.register(proto); + workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); } @Test public void emptyPoolShouldCreateNewWorkdir() throws IOException { - SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); File masterRepo = createRepositoryDirectory(); try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { @@ -59,7 +62,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { @Test public void cloneFromPoolShouldNotBeReused() throws IOException { - SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); File firstDirectory; try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { @@ -73,7 +76,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { @Test public void cloneFromPoolShouldBeDeletedOnClose() throws IOException { - SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); File directory; try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java index 9565f672d5..b9194145e6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java @@ -4,6 +4,7 @@ import com.aragost.javahg.Repository; import com.aragost.javahg.commands.CloneCommand; import com.aragost.javahg.commands.PullCommand; import sonia.scm.repository.util.SimpleWorkdirFactory; +import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.web.HgRepositoryEnvironmentBuilder; import javax.inject.Inject; @@ -18,7 +19,8 @@ public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory hgRepositoryEnvironmentBuilder; @Inject - public SimpleHgWorkdirFactory(Provider hgRepositoryEnvironmentBuilder) { + public SimpleHgWorkdirFactory(Provider hgRepositoryEnvironmentBuilder, WorkdirProvider workdirProvider) { + super(workdirProvider); this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder; } @Override diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java index c015aa0f62..7976af8b3b 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import sonia.scm.repository.Branch; import sonia.scm.repository.HgTestUtil; import sonia.scm.repository.api.BranchRequest; +import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.web.HgRepositoryEnvironmentBuilder; import java.io.IOException; @@ -20,7 +21,7 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase { HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder = new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager()); - SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder)) { + SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder), new WorkdirProvider()) { @Override public void configure(PullCommand pullCommand) { // we do not want to configure http hooks in this unit test From 44d99fcc9d8ff093dccfd55453baddeb35ff9ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 27 Aug 2019 16:57:10 +0200 Subject: [PATCH 12/35] Create new modification command --- .../api/ModificationCommandBuilder.java | 194 ++++++++++++++++++ .../repository/spi/ModificationCommand.java | 15 ++ .../api/ModificationCommandBuilderTest.java | 160 +++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java create mode 100644 scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java new file mode 100644 index 0000000000..863fb1731b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java @@ -0,0 +1,194 @@ +package sonia.scm.repository.api; + +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import sonia.scm.repository.spi.ModificationCommand; +import sonia.scm.repository.util.WorkdirProvider; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class ModificationCommandBuilder { + + private final ModificationCommand command; + private final File workdir; + + private final List modifications = new ArrayList<>(); + + ModificationCommandBuilder(ModificationCommand command, WorkdirProvider workdirProvider) { + this.command = command; + this.workdir = workdirProvider.createNewWorkdir(); + } + + ModificationCommandBuilder setBranchToModify(String branchToModify) { + return this; + } + + ContentLoader createFile(String path) { + return new ContentLoader( + content -> modifications.add(new CreateFile(path, content)) + ); + } + + ContentLoader modifyFile(String path) { + return new ContentLoader( + content -> modifications.add(new ModifyFile(path, content)) + ); + } + + ModificationCommandBuilder deleteFile(String path) { + modifications.add(new DeleteFile(path)); + return this; + } + + ModificationCommandBuilder moveFile(String sourcePath, String targetPath) { + modifications.add(new MoveFile(sourcePath, targetPath)); + return this; + } + + String execute() { + modifications.forEach(FileModification::execute); + modifications.forEach(FileModification::cleanup); + return command.commit(); + } + + private Content loadData(ByteSource data) throws IOException { + File file = createTemporaryFile(); + data.copyTo(Files.asByteSink(file)); + return new Content(file); + } + + private Content loadData(InputStream data) throws IOException { + File file = createTemporaryFile(); + OutputStream out = Files.asByteSink(file).openBufferedStream(); + ByteStreams.copy(data, out); + out.close(); + return new Content(file); + } + + private File createTemporaryFile() throws IOException { + return File.createTempFile("upload-", "", workdir); + } + + private interface FileModification { + void execute(); + + default void cleanup() { + } + } + + private class DeleteFile implements FileModification { + private final String path; + + public DeleteFile(String path) { + this.path = path; + } + + @Override + public void execute() { + command.delete(path); + } + } + + private class MoveFile implements FileModification { + private final String sourcePath; + private final String targetPath; + + private MoveFile(String sourcePath, String targetPath) { + this.sourcePath = sourcePath; + this.targetPath = targetPath; + } + + @Override + public void execute() { + command.move(sourcePath, targetPath); + } + } + + private abstract class DataBasedModification implements FileModification { + + private final Content content; + + DataBasedModification(Content content) { + this.content = content; + } + + public Content getContent() { + return content; + } + @Override + public void cleanup() { + content.deleteFile(); + } + } + + private class CreateFile extends DataBasedModification { + + private final String path; + + CreateFile(String path, Content content) { + super(content); + this.path = path; + } + @Override + public void execute() { + command.create(path, getContent().getFile()); + } + } + + private class ModifyFile extends DataBasedModification { + + private final String path; + + ModifyFile(String path, Content content) { + super(content); + this.path = path; + } + @Override + public void execute() { + command.modify(path, getContent().getFile()); + } + } + + public class ContentLoader { + + private final Consumer contentConsumer; + + private ContentLoader(Consumer contentConsumer) { + this.contentConsumer = contentConsumer; + } + + public ModificationCommandBuilder withData(ByteSource data) throws IOException { + Content content = loadData(data); + contentConsumer.accept(content); + return ModificationCommandBuilder.this; + } + public ModificationCommandBuilder withData(InputStream data) throws IOException { + Content content = loadData(data); + contentConsumer.accept(content); + return ModificationCommandBuilder.this; + } + } + + private class Content { + + private final File file; + + Content(File file) { + this.file = file; + } + + private File getFile() { + return file; + } + public void deleteFile() { + file.delete(); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java new file mode 100644 index 0000000000..c221acc1d6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java @@ -0,0 +1,15 @@ +package sonia.scm.repository.spi; + +import java.io.File; + +public interface ModificationCommand { + String commit(); + + void delete(String toBeDeleted); + + void create(String toBeCreated, File file); + + void modify(String path, File file); + + void move(String sourcePath, String targetPath); +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java new file mode 100644 index 0000000000..f27b89964c --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java @@ -0,0 +1,160 @@ +package sonia.scm.repository.api; + +import com.google.common.io.ByteSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; +import sonia.scm.repository.spi.ModificationCommand; +import sonia.scm.repository.util.WorkdirProvider; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) +class ModificationCommandBuilderTest { + + @Mock + ModificationCommand command; + @Mock + WorkdirProvider workdirProvider; + + ModificationCommandBuilder commandBuilder; + + @BeforeEach + void initWorkdir(@TempDirectory.TempDir Path temp) throws IOException { + lenient().when(workdirProvider.createNewWorkdir()).thenReturn(temp.toFile()); + commandBuilder = new ModificationCommandBuilder(command, workdirProvider); + } + + @Test + void shouldReturnTargetRevisionFromCommit() { + when(command.commit()).thenReturn("target"); + + String targetRevision = commandBuilder.execute(); + + assertThat(targetRevision).isEqualTo("target"); + } + + @Test + void shouldExecuteDelete() { + commandBuilder + .deleteFile("toBeDeleted") + .execute(); + + verify(command).delete("toBeDeleted"); + } + + @Test + void shouldExecuteMove() { + commandBuilder + .moveFile("source", "target") + .execute(); + + verify(command).move("source", "target"); + } + + @Test + void shouldExecuteCreateWithByteSourceContent() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + + commandBuilder + .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithInputStreamContent() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + + commandBuilder + .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateMultipleTimes() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + + commandBuilder + .createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes())) + .createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes())) + .execute(); + + List createdNames = nameCaptor.getAllValues(); + assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1"); + assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2"); + assertThat(contentCaptor).contains("content_1", "content_2"); + } + + @Test + void shouldExecuteModify() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(command).modify(nameCaptor.capture(), any()); + + commandBuilder + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeModified"); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldDeleteTemporaryFiles(@TempDirectory.TempDir Path temp) throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + doNothing().when(command).modify(nameCaptor.capture(), fileCaptor.capture()); + + commandBuilder + .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) + .execute(); + + assertThat(Files.list(temp)).isEmpty(); + } + + private static class ExtractContent implements Answer { + private final List contentCaptor; + + public ExtractContent(List contentCaptor) { + this.contentCaptor = contentCaptor; + } + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return contentCaptor.add(Files.readAllLines(((File) invocation.getArgument(1)).toPath()).get(0)); + } + } +} From 956d9a42c8a3eb6ad17d518ce78422128b6d037b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 06:32:15 +0200 Subject: [PATCH 13/35] Fix injection --- .../java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java index 28042f0834..9edcf0c0ea 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java @@ -9,10 +9,12 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.util.SimpleWorkdirFactory; import sonia.scm.repository.util.WorkdirProvider; +import javax.inject.Inject; import java.io.File; public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory implements GitWorkdirFactory { + @Inject public SimpleGitWorkdirFactory(WorkdirProvider workdirProvider) { super(workdirProvider); } From 57e748b4e4ef2bd45dc5239d9947422a0964ba50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 06:43:18 +0200 Subject: [PATCH 14/35] Rename class to distinguish between 'modifications' and 'modify' --- ...Builder.java => ModifyCommandBuilder.java} | 22 +++++++++---------- ...icationCommand.java => ModifyCommand.java} | 2 +- ...est.java => ModifyCommandBuilderTest.java} | 10 ++++----- 3 files changed, 17 insertions(+), 17 deletions(-) rename scm-core/src/main/java/sonia/scm/repository/api/{ModificationCommandBuilder.java => ModifyCommandBuilder.java} (85%) rename scm-core/src/main/java/sonia/scm/repository/spi/{ModificationCommand.java => ModifyCommand.java} (86%) rename scm-core/src/test/java/sonia/scm/repository/api/{ModificationCommandBuilderTest.java => ModifyCommandBuilderTest.java} (95%) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java similarity index 85% rename from scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java rename to scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 863fb1731b..9653aed934 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModificationCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -3,7 +3,7 @@ package sonia.scm.repository.api; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import com.google.common.io.Files; -import sonia.scm.repository.spi.ModificationCommand; +import sonia.scm.repository.spi.ModifyCommand; import sonia.scm.repository.util.WorkdirProvider; import java.io.File; @@ -14,19 +14,19 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -public class ModificationCommandBuilder { +public class ModifyCommandBuilder { - private final ModificationCommand command; + private final ModifyCommand command; private final File workdir; private final List modifications = new ArrayList<>(); - ModificationCommandBuilder(ModificationCommand command, WorkdirProvider workdirProvider) { + ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider) { this.command = command; this.workdir = workdirProvider.createNewWorkdir(); } - ModificationCommandBuilder setBranchToModify(String branchToModify) { + ModifyCommandBuilder setBranchToModify(String branchToModify) { return this; } @@ -42,12 +42,12 @@ public class ModificationCommandBuilder { ); } - ModificationCommandBuilder deleteFile(String path) { + ModifyCommandBuilder deleteFile(String path) { modifications.add(new DeleteFile(path)); return this; } - ModificationCommandBuilder moveFile(String sourcePath, String targetPath) { + ModifyCommandBuilder moveFile(String sourcePath, String targetPath) { modifications.add(new MoveFile(sourcePath, targetPath)); return this; } @@ -164,15 +164,15 @@ public class ModificationCommandBuilder { this.contentConsumer = contentConsumer; } - public ModificationCommandBuilder withData(ByteSource data) throws IOException { + public ModifyCommandBuilder withData(ByteSource data) throws IOException { Content content = loadData(data); contentConsumer.accept(content); - return ModificationCommandBuilder.this; + return ModifyCommandBuilder.this; } - public ModificationCommandBuilder withData(InputStream data) throws IOException { + public ModifyCommandBuilder withData(InputStream data) throws IOException { Content content = loadData(data); contentConsumer.accept(content); - return ModificationCommandBuilder.this; + return ModifyCommandBuilder.this; } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java similarity index 86% rename from scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java rename to scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java index c221acc1d6..8843965295 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java @@ -2,7 +2,7 @@ package sonia.scm.repository.spi; import java.io.File; -public interface ModificationCommand { +public interface ModifyCommand { String commit(); void delete(String toBeDeleted); diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java similarity index 95% rename from scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java rename to scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java index f27b89964c..86e8dc4fc1 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/ModificationCommandBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java @@ -10,7 +10,7 @@ import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; -import sonia.scm.repository.spi.ModificationCommand; +import sonia.scm.repository.spi.ModifyCommand; import sonia.scm.repository.util.WorkdirProvider; import java.io.ByteArrayInputStream; @@ -31,19 +31,19 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @ExtendWith(TempDirectory.class) -class ModificationCommandBuilderTest { +class ModifyCommandBuilderTest { @Mock - ModificationCommand command; + ModifyCommand command; @Mock WorkdirProvider workdirProvider; - ModificationCommandBuilder commandBuilder; + ModifyCommandBuilder commandBuilder; @BeforeEach void initWorkdir(@TempDirectory.TempDir Path temp) throws IOException { lenient().when(workdirProvider.createNewWorkdir()).thenReturn(temp.toFile()); - commandBuilder = new ModificationCommandBuilder(command, workdirProvider); + commandBuilder = new ModifyCommandBuilder(command, workdirProvider); } @Test From c57ef73475bfe7005c85004b3d196f30d22d5844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 12:58:56 +0200 Subject: [PATCH 15/35] Introduce command request for modify command --- .../repository/api/ModifyCommandBuilder.java | 135 ++++-------------- .../scm/repository/spi/ModifyCommand.java | 13 +- .../repository/spi/ModifyCommandRequest.java | 133 +++++++++++++++++ .../api/ModifyCommandBuilderTest.java | 73 +++++++--- 4 files changed, 224 insertions(+), 130 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 9653aed934..dc3ed4a331 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -1,17 +1,18 @@ package sonia.scm.repository.api; +import com.google.common.base.Preconditions; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import com.google.common.io.Files; +import sonia.scm.repository.Person; import sonia.scm.repository.spi.ModifyCommand; +import sonia.scm.repository.spi.ModifyCommandRequest; import sonia.scm.repository.util.WorkdirProvider; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; import java.util.function.Consumer; public class ModifyCommandBuilder { @@ -19,7 +20,7 @@ public class ModifyCommandBuilder { private final ModifyCommand command; private final File workdir; - private final List modifications = new ArrayList<>(); + private final ModifyCommandRequest request = new ModifyCommandRequest(); ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider) { this.command = command; @@ -32,163 +33,81 @@ public class ModifyCommandBuilder { ContentLoader createFile(String path) { return new ContentLoader( - content -> modifications.add(new CreateFile(path, content)) + content -> request.addRequest(new ModifyCommandRequest.CreateFileRequest(path, content)) ); } ContentLoader modifyFile(String path) { return new ContentLoader( - content -> modifications.add(new ModifyFile(path, content)) + content -> request.addRequest(new ModifyCommandRequest.ModifyFileRequest(path, content)) ); } ModifyCommandBuilder deleteFile(String path) { - modifications.add(new DeleteFile(path)); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest(path)); return this; } ModifyCommandBuilder moveFile(String sourcePath, String targetPath) { - modifications.add(new MoveFile(sourcePath, targetPath)); + request.addRequest(new ModifyCommandRequest.MoveFileRequest(sourcePath, targetPath)); return this; } String execute() { - modifications.forEach(FileModification::execute); - modifications.forEach(FileModification::cleanup); - return command.commit(); + Preconditions.checkArgument(request.isValid(), "commit message, author and branch are required"); + return command.execute(request); } - private Content loadData(ByteSource data) throws IOException { + private File loadData(ByteSource data) throws IOException { File file = createTemporaryFile(); data.copyTo(Files.asByteSink(file)); - return new Content(file); + return file; } - private Content loadData(InputStream data) throws IOException { + private File loadData(InputStream data) throws IOException { File file = createTemporaryFile(); OutputStream out = Files.asByteSink(file).openBufferedStream(); ByteStreams.copy(data, out); out.close(); - return new Content(file); + return file; } private File createTemporaryFile() throws IOException { return File.createTempFile("upload-", "", workdir); } - private interface FileModification { - void execute(); - - default void cleanup() { - } + public ModifyCommandBuilder setCommitMessage(String message) { + request.setCommitMessage(message); + return this; } - private class DeleteFile implements FileModification { - private final String path; - - public DeleteFile(String path) { - this.path = path; - } - - @Override - public void execute() { - command.delete(path); - } + public ModifyCommandBuilder setAuthor(Person author) { + request.setAuthor(author); + return this; } - private class MoveFile implements FileModification { - private final String sourcePath; - private final String targetPath; - - private MoveFile(String sourcePath, String targetPath) { - this.sourcePath = sourcePath; - this.targetPath = targetPath; - } - - @Override - public void execute() { - command.move(sourcePath, targetPath); - } - } - - private abstract class DataBasedModification implements FileModification { - - private final Content content; - - DataBasedModification(Content content) { - this.content = content; - } - - public Content getContent() { - return content; - } - @Override - public void cleanup() { - content.deleteFile(); - } - } - - private class CreateFile extends DataBasedModification { - - private final String path; - - CreateFile(String path, Content content) { - super(content); - this.path = path; - } - @Override - public void execute() { - command.create(path, getContent().getFile()); - } - } - - private class ModifyFile extends DataBasedModification { - - private final String path; - - ModifyFile(String path, Content content) { - super(content); - this.path = path; - } - @Override - public void execute() { - command.modify(path, getContent().getFile()); - } + public ModifyCommandBuilder setBranch(String branch) { + request.setBranch(branch); + return this; } public class ContentLoader { - private final Consumer contentConsumer; + private final Consumer contentConsumer; - private ContentLoader(Consumer contentConsumer) { + private ContentLoader(Consumer contentConsumer) { this.contentConsumer = contentConsumer; } public ModifyCommandBuilder withData(ByteSource data) throws IOException { - Content content = loadData(data); + File content = loadData(data); contentConsumer.accept(content); return ModifyCommandBuilder.this; } public ModifyCommandBuilder withData(InputStream data) throws IOException { - Content content = loadData(data); + File content = loadData(data); contentConsumer.accept(content); return ModifyCommandBuilder.this; } } - - private class Content { - - private final File file; - - Content(File file) { - this.file = file; - } - - private File getFile() { - return file; - } - public void deleteFile() { - file.delete(); - } - } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java index 8843965295..07b29e8d21 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java @@ -3,13 +3,16 @@ package sonia.scm.repository.spi; import java.io.File; public interface ModifyCommand { - String commit(); - void delete(String toBeDeleted); + String execute(ModifyCommandRequest request); - void create(String toBeCreated, File file); + interface Worker { + void delete(String toBeDeleted); - void modify(String path, File file); + void create(String toBeCreated, File file); - void move(String sourcePath, String targetPath); + void modify(String path, File file); + + void move(String sourcePath, String targetPath); + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java new file mode 100644 index 0000000000..057ec2d875 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -0,0 +1,133 @@ +package sonia.scm.repository.spi; + +import org.apache.commons.lang.StringUtils; +import sonia.scm.Validateable; +import sonia.scm.repository.Person; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ModifyCommandRequest implements Resetable, Validateable { + + private final List requests = new ArrayList<>(); + + private Person author; + private String commitMessage; + private String branch; + + @Override + public void reset() { + requests.clear(); + author = null; + commitMessage = null; + branch = null; + } + + public void addRequest(PartialRequest request) { + this.requests.add(request); + } + + public void setAuthor(Person author) { + this.author = author; + } + + public void setCommitMessage(String commitMessage) { + this.commitMessage = commitMessage; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public List getRequests() { + return Collections.unmodifiableList(requests); + } + + @Override + public boolean isValid() { + return StringUtils.isNotEmpty(commitMessage) && StringUtils.isNotEmpty(branch) && author != null && !requests.isEmpty(); + } + + public interface PartialRequest { + void execute(ModifyCommand.Worker worker); + } + + public static class DeleteFileRequest implements PartialRequest { + private final String path; + + public DeleteFileRequest(String path) { + this.path = path; + } + + @Override + public void execute(ModifyCommand.Worker worker) { + worker.delete(path); + } + } + + public static class MoveFileRequest implements PartialRequest { + private final String sourcePath; + private final String targetPath; + + public MoveFileRequest(String sourcePath, String targetPath) { + this.sourcePath = sourcePath; + this.targetPath = targetPath; + } + + @Override + public void execute(ModifyCommand.Worker worker) { + worker.move(sourcePath, targetPath); + } + } + + private abstract static class ContentModificationRequest implements PartialRequest { + + private final File content; + + ContentModificationRequest(File content) { + this.content = content; + } + + File getContent() { + return content; + } + + void cleanup() { + content.delete(); // TODO Handle errors + } + } + + public static class CreateFileRequest extends ContentModificationRequest { + + private final String path; + + public CreateFileRequest(String path, File content) { + super(content); + this.path = path; + } + + @Override + public void execute(ModifyCommand.Worker worker) { + worker.create(path, getContent()); + cleanup(); + } + } + + public static class ModifyFileRequest extends ContentModificationRequest { + + private final String path; + + public ModifyFileRequest(String path, File content) { + super(content); + this.path = path; + } + + @Override + public void execute(ModifyCommand.Worker worker) { + worker.modify(path, getContent()); + cleanup(); + } + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java index 86e8dc4fc1..adbefe7d2a 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java @@ -6,11 +6,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; +import sonia.scm.repository.Person; import sonia.scm.repository.spi.ModifyCommand; +import sonia.scm.repository.spi.ModifyCommandRequest; import sonia.scm.repository.util.WorkdirProvider; import java.io.ByteArrayInputStream; @@ -37,6 +40,11 @@ class ModifyCommandBuilderTest { ModifyCommand command; @Mock WorkdirProvider workdirProvider; + @Mock + ModifyCommand.Worker worker; + + @Captor + ArgumentCaptor requestCaptor; ModifyCommandBuilder commandBuilder; @@ -46,43 +54,54 @@ class ModifyCommandBuilderTest { commandBuilder = new ModifyCommandBuilder(command, workdirProvider); } + @BeforeEach + void initRequestCaptor() { + when(command.execute(requestCaptor.capture())).thenReturn("target"); + } + @Test void shouldReturnTargetRevisionFromCommit() { - when(command.commit()).thenReturn("target"); - - String targetRevision = commandBuilder.execute(); + String targetRevision = initCommand() + .deleteFile("toBeDeleted") + .execute(); assertThat(targetRevision).isEqualTo("target"); } @Test void shouldExecuteDelete() { - commandBuilder + initCommand() .deleteFile("toBeDeleted") .execute(); - verify(command).delete("toBeDeleted"); + executeRequest(); + + verify(worker).delete("toBeDeleted"); } @Test void shouldExecuteMove() { - commandBuilder + initCommand() .moveFile("source", "target") .execute(); - verify(command).move("source", "target"); + executeRequest(); + + verify(worker).move("source", "target"); } @Test void shouldExecuteCreateWithByteSourceContent() throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any()); - commandBuilder + initCommand() .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes())) .execute(); + executeRequest(); + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); assertThat(contentCaptor).contains("content"); } @@ -91,12 +110,14 @@ class ModifyCommandBuilderTest { void shouldExecuteCreateWithInputStreamContent() throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any()); - commandBuilder + initCommand() .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) .execute(); + executeRequest(); + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); assertThat(contentCaptor).contains("content"); } @@ -105,13 +126,15 @@ class ModifyCommandBuilderTest { void shouldExecuteCreateMultipleTimes() throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(command).create(nameCaptor.capture(), any()); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any()); - commandBuilder + initCommand() .createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes())) .createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes())) .execute(); + executeRequest(); + List createdNames = nameCaptor.getAllValues(); assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1"); assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2"); @@ -122,26 +145,42 @@ class ModifyCommandBuilderTest { void shouldExecuteModify() throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(command).modify(nameCaptor.capture(), any()); + doAnswer(new ExtractContent(contentCaptor)).when(worker).modify(nameCaptor.capture(), any()); - commandBuilder + initCommand() .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) .execute(); + executeRequest(); + assertThat(nameCaptor.getValue()).isEqualTo("toBeModified"); assertThat(contentCaptor).contains("content"); } + private void executeRequest() { + ModifyCommandRequest request = requestCaptor.getValue(); + request.getRequests().forEach(r -> r.execute(worker)); + } + + private ModifyCommandBuilder initCommand() { + return commandBuilder + .setBranch("branch") + .setCommitMessage("message") + .setAuthor(new Person()); + } + @Test void shouldDeleteTemporaryFiles(@TempDirectory.TempDir Path temp) throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); - doNothing().when(command).modify(nameCaptor.capture(), fileCaptor.capture()); + doNothing().when(worker).modify(nameCaptor.capture(), fileCaptor.capture()); - commandBuilder + initCommand() .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) .execute(); + executeRequest(); + assertThat(Files.list(temp)).isEmpty(); } From a8ce0f028d6a2fc55b05616f98c2f4c0ec1461ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 13:03:51 +0200 Subject: [PATCH 16/35] Add missing getter in reuest --- .../scm/repository/spi/ModifyCommandRequest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index 057ec2d875..f3dcca18ff 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -45,6 +45,18 @@ public class ModifyCommandRequest implements Resetable, Validateable { return Collections.unmodifiableList(requests); } + public Person getAuthor() { + return author; + } + + public String getCommitMessage() { + return commitMessage; + } + + public String getBranch() { + return branch; + } + @Override public boolean isValid() { return StringUtils.isNotEmpty(commitMessage) && StringUtils.isNotEmpty(branch) && author != null && !requests.isEmpty(); From e0ecb3440bc9db4c2f3bf176efb5c2410af1a80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 13:19:59 +0200 Subject: [PATCH 17/35] Add modify command --- scm-core/src/main/java/sonia/scm/repository/api/Command.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index 3249e54ec3..8e95e314bb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -66,5 +66,5 @@ public enum Command /** * @since 2.0 */ - MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH; + MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH, MODIFY; } From baa4f6a91731ffb77d8b71522c4be4ee0d084dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 13:53:24 +0200 Subject: [PATCH 18/35] Make modify command available --- .../repository/api/ModifyCommandBuilder.java | 160 +++++++++++++----- .../scm/repository/api/RepositoryService.java | 31 +++- .../api/RepositoryServiceFactory.java | 5 +- .../spi/RepositoryServiceProvider.java | 8 + .../repository/api/RepositoryServiceTest.java | 6 +- 5 files changed, 162 insertions(+), 48 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index dc3ed4a331..501a2ac3b0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -15,6 +15,29 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.function.Consumer; +/** + * Use this {@link ModifyCommandBuilder} to make file changes to the head of a branch. You can + *
    + *
  • create new files ({@link #createFile(String)}
  • + *
  • modify existing files ({@link #modifyFile(String)}
  • + *
  • delete existing files ({@link #deleteFile(String)}
  • + *
  • move/rename existing files ({@link #moveFile(String, String)}
  • + *
+ * + * You can collect multiple changes before they are executed with a call to {@link #execute()}. + * + *

Example: + *

+ * commandBuilder
+ *     .setBranch("feature/branch")
+ *     .setCommitMessage("make some changes")
+ *     .setAuthor(new Person())
+ *     .createFile("file/to/create").withData(inputStream)
+ *     .deleteFile("old/file/to/delete")
+ *     .execute();
+ * 
+ *

+ */ public class ModifyCommandBuilder { private final ModifyCommand command; @@ -27,37 +50,127 @@ public class ModifyCommandBuilder { this.workdir = workdirProvider.createNewWorkdir(); } - ModifyCommandBuilder setBranchToModify(String branchToModify) { + /** + * Set the branch that should be modified. The new commit will be made for this branch. + * @param branchToModify The branch to modify. + * @return This builder instance. + */ + public ModifyCommandBuilder setBranchToModify(String branchToModify) { return this; } - ContentLoader createFile(String path) { + /** + * Create a new file. The content of the file will be specified in a subsequent call to + * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}. + * @param path The path and the name of the file that should be created. + * @return The loader to specify the content of the new file. + */ + public ContentLoader createFile(String path) { return new ContentLoader( content -> request.addRequest(new ModifyCommandRequest.CreateFileRequest(path, content)) ); } - ContentLoader modifyFile(String path) { + /** + * Modify an existing file. The new content of the file will be specified in a subsequent call to + * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}. + * @param path The path and the name of the file that should be modified. + * @return The loader to specify the new content of the file. + */ + public ContentLoader modifyFile(String path) { return new ContentLoader( content -> request.addRequest(new ModifyCommandRequest.ModifyFileRequest(path, content)) ); } - ModifyCommandBuilder deleteFile(String path) { + /** + * Delete an existing file. + * @param path The path and the name of the file that should be deleted. + * @return This builder instance. + */ + public ModifyCommandBuilder deleteFile(String path) { request.addRequest(new ModifyCommandRequest.DeleteFileRequest(path)); return this; } - ModifyCommandBuilder moveFile(String sourcePath, String targetPath) { + /** + * Move an existing file. + * @param sourcePath The path and the name of the file that should be moved. + * @param targetPath The new path and name the file should be moved to. + * @return This builder instance. + */ + public ModifyCommandBuilder moveFile(String sourcePath, String targetPath) { request.addRequest(new ModifyCommandRequest.MoveFileRequest(sourcePath, targetPath)); return this; } - String execute() { + /** + * Apply the changes and create a new commit with the given message and author. + * @return The revision of the new commit. + */ + public String execute() { Preconditions.checkArgument(request.isValid(), "commit message, author and branch are required"); return command.execute(request); } + /** + * Set the commit message for the new commit. + * @return This builder instance. + */ + public ModifyCommandBuilder setCommitMessage(String message) { + request.setCommitMessage(message); + return this; + } + + /** + * Set the author for the new commit. + * @return This builder instance. + */ + public ModifyCommandBuilder setAuthor(Person author) { + request.setAuthor(author); + return this; + } + + /** + * Set the branch the changes should be made upon. + * @return This builder instance. + */ + public ModifyCommandBuilder setBranch(String branch) { + request.setBranch(branch); + return this; + } + + public class ContentLoader { + + private final Consumer contentConsumer; + + private ContentLoader(Consumer contentConsumer) { + this.contentConsumer = contentConsumer; + } + + /** + * Specify the data of the file using a {@link ByteSource}. + * @return The builder instance. + * @throws IOException If the data could not be read. + */ + public ModifyCommandBuilder withData(ByteSource data) throws IOException { + File content = loadData(data); + contentConsumer.accept(content); + return ModifyCommandBuilder.this; + } + + /** + * Specify the data of the file using an {@link InputStream}. + * @return The builder instance. + * @throws IOException If the data could not be read. + */ + public ModifyCommandBuilder withData(InputStream data) throws IOException { + File content = loadData(data); + contentConsumer.accept(content); + return ModifyCommandBuilder.this; + } + } + private File loadData(ByteSource data) throws IOException { File file = createTemporaryFile(); data.copyTo(Files.asByteSink(file)); @@ -75,39 +188,4 @@ public class ModifyCommandBuilder { private File createTemporaryFile() throws IOException { return File.createTempFile("upload-", "", workdir); } - - public ModifyCommandBuilder setCommitMessage(String message) { - request.setCommitMessage(message); - return this; - } - - public ModifyCommandBuilder setAuthor(Person author) { - request.setAuthor(author); - return this; - } - - public ModifyCommandBuilder setBranch(String branch) { - request.setBranch(branch); - return this; - } - - public class ContentLoader { - - private final Consumer contentConsumer; - - private ContentLoader(Consumer contentConsumer) { - this.contentConsumer = contentConsumer; - } - - public ModifyCommandBuilder withData(ByteSource data) throws IOException { - File content = loadData(data); - contentConsumer.accept(content); - return ModifyCommandBuilder.this; - } - public ModifyCommandBuilder withData(InputStream data) throws IOException { - File content = loadData(data); - contentConsumer.accept(content); - return ModifyCommandBuilder.this; - } - } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index 5807ffa998..080d66ebf2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -40,6 +40,7 @@ import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; +import sonia.scm.repository.util.WorkdirProvider; import java.io.Closeable; import java.io.IOException; @@ -91,22 +92,25 @@ public final class RepositoryService implements Closeable { private final RepositoryServiceProvider provider; private final Repository repository; private final Set protocolProviders; + private final WorkdirProvider workdirProvider; /** * Constructs a new {@link RepositoryService}. This constructor should only * be called from the {@link RepositoryServiceFactory}. - * @param cacheManager cache manager + * @param cacheManager cache manager * @param provider implementation for {@link RepositoryServiceProvider} * @param repository the repository + * @param workdirProvider */ RepositoryService(CacheManager cacheManager, - RepositoryServiceProvider provider, Repository repository, - PreProcessorUtil preProcessorUtil, Set protocolProviders) { + RepositoryServiceProvider provider, Repository repository, + PreProcessorUtil preProcessorUtil, Set protocolProviders, WorkdirProvider workdirProvider) { this.cacheManager = cacheManager; this.provider = provider; this.repository = repository; this.preProcessorUtil = preProcessorUtil; this.protocolProviders = protocolProviders; + this.workdirProvider = workdirProvider; } /** @@ -399,6 +403,27 @@ public final class RepositoryService implements Closeable { return new MergeCommandBuilder(provider.getMergeCommand()); } + /** + * The modify command makes changes to the head of a branch. It is possible to + *
    + *
  • create new files
  • + *
  • delete existing files
  • + *
  • modify/replace files
  • + *
  • move files
  • + *
+ * + * @return instance of {@link ModifyCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + * @since 2.0.0 + */ + public ModifyCommandBuilder getModifyCommand() { + LOG.debug("create modify command for repository {}", + repository.getNamespaceAndName()); + + return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider); + } + /** * Returns true if the command is supported by the repository service. * diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index 8db3ab7546..fe2af9f9e1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -61,6 +61,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceResolver; +import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.security.ScmSecurityException; import java.util.Set; @@ -256,7 +257,7 @@ public final class RepositoryServiceFactory } service = new RepositoryService(cacheManager, provider, repository, - preProcessorUtil, protocolProviders); + preProcessorUtil, protocolProviders, workdirProvider); break; } @@ -373,4 +374,6 @@ public final class RepositoryServiceFactory private final Set resolvers; private Set protocolProviders; + + private WorkdirProvider workdirProvider; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index bf9cdf6a25..cdd0417cf7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -275,4 +275,12 @@ public abstract class RepositoryServiceProvider implements Closeable { throw new CommandNotSupportedException(Command.MERGE); } + + /** + * @since 2.0 + */ + public ModifyCommand getModifyCommand() + { + throw new CommandNotSupportedException(Command.MODIFY); + } } diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java index 2ceafc19bb..330a4c956a 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java @@ -24,7 +24,7 @@ public class RepositoryServiceTest { @Test public void shouldReturnMatchingProtocolsFromProvider() { - RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider())); + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null); Stream supportedProtocols = repositoryService.getSupportedProtocols(); assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1); @@ -32,7 +32,7 @@ public class RepositoryServiceTest { @Test public void shouldFindKnownProtocol() { - RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider())); + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null); HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); @@ -41,7 +41,7 @@ public class RepositoryServiceTest { @Test public void shouldFailForUnknownProtocol() { - RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider())); + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null); assertThrows(IllegalArgumentException.class, () -> { repositoryService.getProtocol(UnknownScmProtocol.class); From 55ac40363d35b608b374e2db39a0d18554576aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 13:57:39 +0200 Subject: [PATCH 19/35] Add missing injection --- .../scm/repository/api/RepositoryServiceFactory.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index fe2af9f9e1..07c0b8fd47 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -136,13 +136,14 @@ public final class RepositoryServiceFactory * @param resolvers a set of {@link RepositoryServiceResolver} * @param preProcessorUtil helper object for pre processor handling * + * @param workdirProvider * @since 1.21 */ @Inject public RepositoryServiceFactory(ScmConfiguration configuration, - CacheManager cacheManager, RepositoryManager repositoryManager, - Set resolvers, PreProcessorUtil preProcessorUtil, - Set protocolProviders) + CacheManager cacheManager, RepositoryManager repositoryManager, + Set resolvers, PreProcessorUtil preProcessorUtil, + Set protocolProviders, WorkdirProvider workdirProvider) { this.configuration = configuration; this.cacheManager = cacheManager; @@ -150,6 +151,7 @@ public final class RepositoryServiceFactory this.resolvers = resolvers; this.preProcessorUtil = preProcessorUtil; this.protocolProviders = protocolProviders; + this.workdirProvider = workdirProvider; ScmEventBus.getInstance().register(new CacheClearHook(cacheManager)); } @@ -375,5 +377,5 @@ public final class RepositoryServiceFactory private Set protocolProviders; - private WorkdirProvider workdirProvider; + private final WorkdirProvider workdirProvider; } From 406620bd9d3c1805f59bf822f9cfb904e49f7894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 15:07:14 +0200 Subject: [PATCH 20/35] First steps to implement create in modify command --- .../scm/repository/spi/ModifyCommand.java | 3 +- .../repository/spi/ModifyCommandRequest.java | 5 +- .../api/ModifyCommandBuilderTest.java | 10 +- .../scm/repository/spi/GitModifyCommand.java | 173 ++++++++++++++++++ .../spi/GitRepositoryServiceProvider.java | 5 + .../repository/spi/GitMergeCommandTest.java | 13 -- .../repository/spi/GitModifyCommandTest.java | 31 ++++ 7 files changed, 220 insertions(+), 20 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java create mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java index 07b29e8d21..a8b3efd6d9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java @@ -1,6 +1,7 @@ package sonia.scm.repository.spi; import java.io.File; +import java.io.IOException; public interface ModifyCommand { @@ -9,7 +10,7 @@ public interface ModifyCommand { interface Worker { void delete(String toBeDeleted); - void create(String toBeCreated, File file); + void create(String toBeCreated, File file) throws IOException; void modify(String path, File file); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index f3dcca18ff..1ef92dab0a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -5,6 +5,7 @@ import sonia.scm.Validateable; import sonia.scm.repository.Person; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -63,7 +64,7 @@ public class ModifyCommandRequest implements Resetable, Validateable { } public interface PartialRequest { - void execute(ModifyCommand.Worker worker); + void execute(ModifyCommand.Worker worker) throws IOException; } public static class DeleteFileRequest implements PartialRequest { @@ -121,7 +122,7 @@ public class ModifyCommandRequest implements Resetable, Validateable { } @Override - public void execute(ModifyCommand.Worker worker) { + public void execute(ModifyCommand.Worker worker) throws IOException { worker.create(path, getContent()); cleanup(); } diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java index adbefe7d2a..e2dfb955a5 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java @@ -69,7 +69,7 @@ class ModifyCommandBuilderTest { } @Test - void shouldExecuteDelete() { + void shouldExecuteDelete() throws IOException { initCommand() .deleteFile("toBeDeleted") .execute(); @@ -80,7 +80,7 @@ class ModifyCommandBuilderTest { } @Test - void shouldExecuteMove() { + void shouldExecuteMove() throws IOException { initCommand() .moveFile("source", "target") .execute(); @@ -157,9 +157,11 @@ class ModifyCommandBuilderTest { assertThat(contentCaptor).contains("content"); } - private void executeRequest() { + private void executeRequest() throws IOException { ModifyCommandRequest request = requestCaptor.getValue(); - request.getRequests().forEach(r -> r.execute(worker)); + for (ModifyCommandRequest.PartialRequest r : request.getRequests()) { + r.execute(worker); + } } private ModifyCommandBuilder initCommand() { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java new file mode 100644 index 0000000000..8256b7e766 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -0,0 +1,173 @@ +package sonia.scm.repository.spi; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.lib.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Person; +import sonia.scm.repository.Repository; +import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.user.User; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand { + + private static final Logger LOG = LoggerFactory.getLogger(GitModifyCommand.class); + + private final GitWorkdirFactory workdirFactory; + + public GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) { + super(context, repository); + this.workdirFactory = workdirFactory; + } + + @Override + public String execute(ModifyCommandRequest request) { + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) { + org.eclipse.jgit.lib.Repository repository = workingCopy.getWorkingRepository(); + LOG.debug("cloned repository to folder {}", repository.getWorkTree()); + try { + return new ModifyWorker(repository, request).execute(); + } catch (IOException e) { + return throwInternalRepositoryException("could not apply modifications to cloned repository", e); + } + } + } + + private class ModifyWorker implements Worker { + + private final Git clone; + private final File workDir; + private final ModifyCommandRequest request; + + ModifyWorker(org.eclipse.jgit.lib.Repository repository, ModifyCommandRequest request) { + this.clone = new Git(repository); + this.workDir = repository.getWorkTree(); + this.request = request; + } + + String execute() throws IOException { + checkOutBranch(); + for (ModifyCommandRequest.PartialRequest r: request.getRequests()) { + r.execute(this); + } + return null; + } + + @Override + public void create(String toBeCreated, File file) throws IOException { + Path targetFile = new File(workDir, toBeCreated).toPath(); + Files.createDirectories(targetFile.getParent()); + Files.copy(file.toPath(), targetFile); + try { + clone.add().addFilepattern(toBeCreated).call(); + } catch (GitAPIException e) { + throwInternalRepositoryException("could not add new file to index", e); + } + } + + @Override + public void delete(String toBeDeleted) { + + } + + @Override + public void modify(String path, File file) { + + } + + @Override + public void move(String sourcePath, String targetPath) { + + } + + private void checkOutBranch() throws IOException { + String branch = request.getBranch(); + try { + clone.checkout().setName(branch).call(); + } catch (RefNotFoundException e) { + LOG.trace("could not checkout branch {} for modifications directly; trying to create local branch", branch, e); + checkOutTargetAsNewLocalBranch(); + } catch (GitAPIException e) { + throwInternalRepositoryException("could not checkout target branch for merge: " + branch, e); + } + } + + private void checkOutTargetAsNewLocalBranch() throws IOException { + String branch = request.getBranch(); + try { + ObjectId targetRevision = resolveRevision(branch); + clone.checkout().setStartPoint(targetRevision.getName()).setName(branch).setCreateBranch(true).call(); + } catch (RefNotFoundException e) { + LOG.debug("could not checkout branch {} for modifications as local branch", branch, e); + throw notFound(entity("Branch", branch).in(context.getRepository())); + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not checkout branch for modifications as local branch: " + branch, e); + } + } + + private ObjectId resolveRevision(String revision) throws IOException { + ObjectId resolved = clone.getRepository().resolve(revision); + if (resolved == null) { + return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision); + } else { + return resolved; + } + } + + private ObjectId resolveRevisionOrThrowNotFound(org.eclipse.jgit.lib.Repository repository, String revision) throws IOException { + ObjectId resolved = repository.resolve(revision); + if (resolved == null) { + throw notFound(entity("Revision", revision).in(context.getRepository())); + } else { + return resolved; + } + } + + private void doCommit() { + String branch = request.getBranch(); + LOG.debug("modified branch {}", branch); + Person authorToUse = determineAuthor(); + try { + if (!clone.status().call().isClean()) { + clone.commit() + .setAuthor(authorToUse.getName(), authorToUse.getMail()) + .setMessage(request.getCommitMessage()) + .call(); + } + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not commit modifications on branch " + request.getBranch(), e); + } + } + + private Person determineAuthor() { + if (request.getAuthor() == null) { + Subject subject = SecurityUtils.getSubject(); + User user = subject.getPrincipals().oneByType(User.class); + String name = user.getDisplayName(); + String email = user.getMail(); + LOG.debug("no author set; using logged in user: {} <{}>", name, email); + return new Person(name, email); + } else { + return request.getAuthor(); + } + } + } + + private String throwInternalRepositoryException(String message, Exception e) { + throw new InternalRepositoryException(context.getRepository(), message, e); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index 6870c85dea..22a9fdf5f3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -268,6 +268,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitMergeCommand(context, repository, handler.getWorkdirFactory()); } + @Override + public ModifyCommand getModifyCommand() { + return new GitModifyCommand(context, repository, handler.getWorkdirFactory()); + } + @Override public Set getSupportedFeatures() { return FEATURES; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index 8b3b4548d9..674da396b5 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -10,30 +10,17 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.transport.ScmTransportProtocol; -import org.eclipse.jgit.transport.Transport; -import org.junit.After; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import sonia.scm.NotFoundException; -import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Person; -import sonia.scm.repository.PreProcessorUtil; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.MergeCommandResult; -import sonia.scm.repository.api.MergeDryRunCommandResult; import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.user.User; import java.io.IOException; -import static com.google.inject.util.Providers.of; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; @SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") public class GitMergeCommandTest extends AbstractGitCommandTestBase { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java new file mode 100644 index 0000000000..3c55849219 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -0,0 +1,31 @@ +package sonia.scm.repository.spi; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.util.WorkdirProvider; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class GitModifyCommandTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + + @Test + public void shouldCreateNewFile() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setBranch("master"); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new/file", newFile)); + command.execute(request); + } +} From b0267d69097f3964cfa466b45bc1568a091fb0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 15:10:38 +0200 Subject: [PATCH 21/35] Handle error on delete --- .../scm/repository/spi/ModifyCommandRequest.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index f3dcca18ff..e8f2223bdd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -1,16 +1,22 @@ package sonia.scm.repository.spi; import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.Validateable; import sonia.scm.repository.Person; +import sonia.scm.util.IOUtil; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class ModifyCommandRequest implements Resetable, Validateable { + private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandRequest.class); + private final List requests = new ArrayList<>(); private Person author; @@ -107,7 +113,11 @@ public class ModifyCommandRequest implements Resetable, Validateable { } void cleanup() { - content.delete(); // TODO Handle errors + try { + IOUtil.delete(content); + } catch (IOException e) { + LOG.warn("could not delete temporary file {}", content, e); + } } } From aae9ba6a59922bfa21859bc260d518d800fad4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 16:23:33 +0200 Subject: [PATCH 22/35] Commit and push changes --- .../scm/repository/spi/GitModifyCommand.java | 14 +++- .../repository/spi/GitModifyCommandTest.java | 76 ++++++++++++++++++- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index 8256b7e766..91c7700737 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -64,7 +64,9 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman for (ModifyCommandRequest.PartialRequest r: request.getRequests()) { r.execute(this); } - return null; + doCommit(); + push(); + return clone.getRepository().getRefDatabase().findRef("HEAD").getObjectId().name(); } @Override @@ -165,6 +167,16 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman return request.getAuthor(); } } + + private void push() { + try { + clone.push().call(); + } catch (GitAPIException e) { + throw new IntegrateChangesFromWorkdirException(repository, + "could not push modified branch " + request.getBranch() + " into central repository", e); + } + LOG.debug("pushed modified branch {}", request.getBranch()); + } } private String throwInternalRepositoryException(String message, Exception e) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index 3c55849219..5001c87e9b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -1,31 +1,99 @@ package sonia.scm.repository.spi; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.Person; import sonia.scm.repository.util.WorkdirProvider; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import static org.assertj.core.api.Assertions.assertThat; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") public class GitModifyCommandTest extends AbstractGitCommandTestBase { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Rule public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + @Rule + public ShiroRule shiro = new ShiroRule(); @Test - public void shouldCreateNewFile() throws IOException { + public void shouldCreateCommit() throws IOException, GitAPIException { File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); - GitModifyCommand command = new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + GitModifyCommand command = createCommand(); ModifyCommandRequest request = new ModifyCommandRequest(); request.setBranch("master"); request.setCommitMessage("test commit"); - request.addRequest(new ModifyCommandRequest.CreateFileRequest("new/file", newFile)); - command.execute(request); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + String newRef = command.execute(request); + + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getFullMessage()).isEqualTo("test commit"); + assertThat(lastCommit.getAuthorIdent().getName()).isEqualTo("Dirk Gently"); + assertThat(newRef).isEqualTo(lastCommit.toObjectId().name()); + } + } + + @Test + public void shouldCreateNewFile() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setBranch("master"); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("new_file")).isTrue(); + + assertInTree(assertions); + } + + private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + try (RevWalk walk = new RevWalk(git.getRepository())) { + RevCommit commit = walk.parseCommit(lastCommit); + ObjectId treeId = commit.getTree().getId(); + try (ObjectReader reader = git.getRepository().newObjectReader()) { + assertions.checkAssertions(new CanonicalTreeParser(null, reader, treeId)); + } + } + } + } + + private RevCommit getLastCommit(Git git) throws GitAPIException { + return git.log().setMaxCount(1).call().iterator().next(); + } + + private GitModifyCommand createCommand() { + return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + } + + @FunctionalInterface + private interface TreeAssertions { + void checkAssertions(CanonicalTreeParser treeParser) throws CorruptObjectException; } } From bb0682fab59dd1163fcf7a43f774b68833137ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Aug 2019 17:04:22 +0200 Subject: [PATCH 23/35] Delete temporary workdir after command was executed --- .../repository/api/ModifyCommandBuilder.java | 17 +++++++-- .../api/ModifyCommandBuilderTest.java | 35 ++++++------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 501a2ac3b0..9cfcaebd6a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -4,10 +4,13 @@ import com.google.common.base.Preconditions; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import com.google.common.io.Files; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.repository.Person; import sonia.scm.repository.spi.ModifyCommand; import sonia.scm.repository.spi.ModifyCommandRequest; import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.util.IOUtil; import java.io.File; import java.io.IOException; @@ -40,6 +43,8 @@ import java.util.function.Consumer; */ public class ModifyCommandBuilder { + private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandBuilder.class); + private final ModifyCommand command; private final File workdir; @@ -109,8 +114,16 @@ public class ModifyCommandBuilder { * @return The revision of the new commit. */ public String execute() { - Preconditions.checkArgument(request.isValid(), "commit message, author and branch are required"); - return command.execute(request); + try { + Preconditions.checkArgument(request.isValid(), "commit message, author and branch are required"); + return command.execute(request); + } finally { + try { + IOUtil.delete(workdir); + } catch (IOException e) { + LOG.warn("could not delete temporary workdir '{}'", workdir, e); + } + } } /** diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java index adbefe7d2a..7d4d96642a 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; @@ -43,20 +42,25 @@ class ModifyCommandBuilderTest { @Mock ModifyCommand.Worker worker; - @Captor - ArgumentCaptor requestCaptor; - ModifyCommandBuilder commandBuilder; + Path workdir; @BeforeEach void initWorkdir(@TempDirectory.TempDir Path temp) throws IOException { - lenient().when(workdirProvider.createNewWorkdir()).thenReturn(temp.toFile()); + workdir = Files.createDirectory(temp.resolve("workdir")); + lenient().when(workdirProvider.createNewWorkdir()).thenReturn(workdir.toFile()); commandBuilder = new ModifyCommandBuilder(command, workdirProvider); } @BeforeEach void initRequestCaptor() { - when(command.execute(requestCaptor.capture())).thenReturn("target"); + when(command.execute(any())).thenAnswer( + invocation -> { + ModifyCommandRequest request = invocation.getArgument(0); + request.getRequests().forEach(r -> r.execute(worker)); + return "target"; + } + ); } @Test @@ -74,8 +78,6 @@ class ModifyCommandBuilderTest { .deleteFile("toBeDeleted") .execute(); - executeRequest(); - verify(worker).delete("toBeDeleted"); } @@ -85,8 +87,6 @@ class ModifyCommandBuilderTest { .moveFile("source", "target") .execute(); - executeRequest(); - verify(worker).move("source", "target"); } @@ -100,8 +100,6 @@ class ModifyCommandBuilderTest { .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes())) .execute(); - executeRequest(); - assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); assertThat(contentCaptor).contains("content"); } @@ -116,8 +114,6 @@ class ModifyCommandBuilderTest { .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) .execute(); - executeRequest(); - assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); assertThat(contentCaptor).contains("content"); } @@ -133,8 +129,6 @@ class ModifyCommandBuilderTest { .createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes())) .execute(); - executeRequest(); - List createdNames = nameCaptor.getAllValues(); assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1"); assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2"); @@ -151,17 +145,10 @@ class ModifyCommandBuilderTest { .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) .execute(); - executeRequest(); - assertThat(nameCaptor.getValue()).isEqualTo("toBeModified"); assertThat(contentCaptor).contains("content"); } - private void executeRequest() { - ModifyCommandRequest request = requestCaptor.getValue(); - request.getRequests().forEach(r -> r.execute(worker)); - } - private ModifyCommandBuilder initCommand() { return commandBuilder .setBranch("branch") @@ -179,8 +166,6 @@ class ModifyCommandBuilderTest { .modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes())) .execute(); - executeRequest(); - assertThat(Files.list(temp)).isEmpty(); } From de7647ba550799eca9ba8546400317ab4b92cd63 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 29 Aug 2019 10:31:25 +0200 Subject: [PATCH 24/35] Extract common code --- .../repository/spi/AbstractGitCommand.java | 128 +++++++++++++++++- .../scm/repository/spi/GitMergeCommand.java | 103 ++------------ .../scm/repository/spi/GitModifyCommand.java | 128 ++---------------- 3 files changed, 143 insertions(+), 216 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index f94ccd59f6..d8c0ff1c60 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -35,15 +35,29 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Strings; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.user.User; import java.io.IOException; import java.util.Optional; +import java.util.function.Function; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; //~--- JDK imports ------------------------------------------------------------ @@ -51,7 +65,7 @@ import java.util.Optional; * * @author Sebastian Sdorra */ -public class AbstractGitCommand +class AbstractGitCommand { /** @@ -66,7 +80,7 @@ public class AbstractGitCommand * @param context * @param repository */ - protected AbstractGitCommand(GitContext context, + AbstractGitCommand(GitContext context, sonia.scm.repository.Repository repository) { this.repository = repository; @@ -83,12 +97,12 @@ public class AbstractGitCommand * * @throws IOException */ - protected Repository open() throws IOException + Repository open() throws IOException { return context.open(); } - protected ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException { + ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException { ObjectId commit; if ( Strings.isNullOrEmpty(requestedCommit) ) { commit = getDefaultBranch(gitRepository); @@ -98,7 +112,7 @@ public class AbstractGitCommand return commit; } - protected ObjectId getDefaultBranch(Repository gitRepository) throws IOException { + ObjectId getDefaultBranch(Repository gitRepository) throws IOException { Ref ref = getBranchOrDefault(gitRepository, null); if (ref == null) { return null; @@ -107,7 +121,7 @@ public class AbstractGitCommand } } - protected Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { + Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { if ( Strings.isNullOrEmpty(requestedBranch) ) { String defaultBranchName = context.getConfig().getDefaultBranch(); if (!Strings.isNullOrEmpty(defaultBranchName)) { @@ -122,6 +136,108 @@ public class AbstractGitCommand } } + > R inClone(Function workerSupplier, GitWorkdirFactory workdirFactory) { + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) { + Repository repository = workingCopy.getWorkingRepository(); + logger.debug("cloned repository to folder {}", repository.getWorkTree()); + return workerSupplier.apply(new Git(repository)).run(); + } catch (IOException e) { + throw new InternalRepositoryException(context.getRepository(), "could not clone repository", e); + } + } + + ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException { + ObjectId resolved = repository.resolve(revision); + if (resolved == null) { + throw notFound(entity("Revision", revision).in(context.getRepository())); + } else { + return resolved; + } + } + + abstract class GitCloneWorker { + private final Git clone; + + GitCloneWorker(Git clone) { + this.clone = clone; + } + + abstract R run() throws IOException; + + Git getClone() { + return clone; + } + + void checkOutBranch(String branchName) throws IOException { + try { + clone.checkout().setName(branchName).call(); + } catch (RefNotFoundException e) { + logger.trace("could not checkout branch {} directly; trying to create local branch", branchName, e); + checkOutTargetAsNewLocalBranch(branchName); + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not checkout branch: " + branchName, e); + } + } + + private void checkOutTargetAsNewLocalBranch(String branchName) throws IOException { + try { + ObjectId targetRevision = resolveRevision(branchName); + clone.checkout().setStartPoint(targetRevision.getName()).setName(branchName).setCreateBranch(true).call(); + } catch (RefNotFoundException e) { + logger.debug("could not checkout branch {} as local branch", branchName, e); + throw notFound(entity("Revision", branchName).in(context.getRepository())); + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not checkout branch as local branch: " + branchName, e); + } + } + + ObjectId resolveRevision(String revision) throws IOException { + ObjectId resolved = clone.getRepository().resolve(revision); + if (resolved == null) { + return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision); + } else { + return resolved; + } + } + + void doCommit(String message, Person author) { + Person authorToUse = determineAuthor(author); + try { + if (!clone.status().call().isClean()) { + clone.commit() + .setAuthor(authorToUse.getName(), authorToUse.getMail()) + .setMessage(message) + .call(); + } + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not commit changes", e); + } + } + + void push() { + try { + clone.push().call(); + } catch (GitAPIException e) { + throw new IntegrateChangesFromWorkdirException(repository, + "could not push changes into central repository", e); + } + logger.debug("pushed changes"); + } + + private Person determineAuthor(Person author) { + if (author == null) { + Subject subject = SecurityUtils.getSubject(); + User user = subject.getPrincipals().oneByType(User.class); + String name = user.getDisplayName(); + String email = user.getMail(); + logger.debug("no author set; using logged in user: {} <{}>", name, email); + return new Person(name, email); + } else { + return author; + } + } + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java index 58c71b8dac..b4a7954b02 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -1,13 +1,10 @@ package sonia.scm.repository.spi; import com.google.common.base.Strings; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.subject.Subject; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeCommand.FastForwardMode; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.merge.MergeStrategy; @@ -17,18 +14,12 @@ import org.slf4j.LoggerFactory; import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Person; -import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; -import sonia.scm.repository.util.WorkingCopy; -import sonia.scm.user.User; import java.io.IOException; import java.text.MessageFormat; -import static sonia.scm.ContextEntry.ContextBuilder.entity; -import static sonia.scm.NotFoundException.notFound; - public class GitMergeCommand extends AbstractGitCommand implements MergeCommand { private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class); @@ -47,13 +38,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand @Override public MergeCommandResult merge(MergeCommandRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) { - Repository repository = workingCopy.getWorkingRepository(); - logger.debug("cloned repository to folder {}", repository.getWorkTree()); - return new MergeWorker(repository, request).merge(); - } catch (IOException e) { - throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e); - } + return inClone(clone -> new MergeWorker(clone, request), workdirFactory); } @Override @@ -70,32 +55,23 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand } } - private ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException { - ObjectId resolved = repository.resolve(revision); - if (resolved == null) { - throw notFound(entity("Revision", revision).in(context.getRepository())); - } else { - return resolved; - } - } - - private class MergeWorker { + private class MergeWorker extends GitCloneWorker { private final String target; private final String toMerge; private final Person author; - private final Git clone; private final String messageTemplate; - private MergeWorker(Repository clone, MergeCommandRequest request) { + private MergeWorker(Git clone, MergeCommandRequest request) { + super(clone); this.target = request.getTargetBranch(); this.toMerge = request.getBranchToMerge(); this.author = request.getAuthor(); this.messageTemplate = request.getMessageTemplate(); - this.clone = new Git(clone); } - private MergeCommandResult merge() throws IOException { + @Override + MergeCommandResult run() throws IOException { checkOutTargetBranch(); MergeResult result = doMergeInClone(); if (result.getMergeStatus().isSuccessful()) { @@ -108,33 +84,14 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand } private void checkOutTargetBranch() throws IOException { - try { - clone.checkout().setName(target).call(); - } catch (RefNotFoundException e) { - logger.trace("could not checkout target branch {} for merge directly; trying to create local branch", target, e); - checkOutTargetAsNewLocalBranch(); - } catch (GitAPIException e) { - throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge: " + target, e); - } - } - - private void checkOutTargetAsNewLocalBranch() throws IOException { - try { - ObjectId targetRevision = resolveRevision(target); - clone.checkout().setStartPoint(targetRevision.getName()).setName(target).setCreateBranch(true).call(); - } catch (RefNotFoundException e) { - logger.debug("could not checkout target branch {} for merge as local branch", target, e); - throw notFound(entity("Revision", target).in(context.getRepository())); - } catch (GitAPIException e) { - throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge as local branch: " + target, e); - } + checkOutBranch(target); } private MergeResult doMergeInClone() throws IOException { MergeResult result; try { ObjectId sourceRevision = resolveRevision(toMerge); - result = clone.merge() + result = getClone().merge() .setFastForward(FastForwardMode.NO_FF) .setCommit(false) // we want to set the author manually .include(toMerge, sourceRevision) @@ -147,17 +104,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand private void doCommit() { logger.debug("merged branch {} into {}", toMerge, target); - Person authorToUse = determineAuthor(); - try { - if (!clone.status().call().isClean()) { - clone.commit() - .setAuthor(authorToUse.getName(), authorToUse.getMail()) - .setMessage(MessageFormat.format(determineMessageTemplate(), toMerge, target)) - .call(); - } - } catch (GitAPIException e) { - throw new InternalRepositoryException(context.getRepository(), "could not commit merge between branch " + toMerge + " and " + target, e); - } + doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author); } private String determineMessageTemplate() { @@ -168,41 +115,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand } } - private Person determineAuthor() { - if (author == null) { - Subject subject = SecurityUtils.getSubject(); - User user = subject.getPrincipals().oneByType(User.class); - String name = user.getDisplayName(); - String email = user.getMail(); - logger.debug("no author set; using logged in user: {} <{}>", name, email); - return new Person(name, email); - } else { - return author; - } - } - - private void push() { - try { - clone.push().call(); - } catch (GitAPIException e) { - throw new IntegrateChangesFromWorkdirException(repository, - "could not push merged branch " + target + " into central repository", e); - } - logger.debug("pushed merged branch {}", target); - } - private MergeCommandResult analyseFailure(MergeResult result) { logger.info("could not merged branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet()); return MergeCommandResult.failure(result.getConflicts().keySet()); } - - private ObjectId resolveRevision(String revision) throws IOException { - ObjectId resolved = clone.getRepository().resolve(revision); - if (resolved == null) { - return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision); - } else { - return resolved; - } - } } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index 91c7700737..8825645a3c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -1,72 +1,50 @@ package sonia.scm.repository.spi; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.subject.Subject; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.api.errors.RefNotFoundException; -import org.eclipse.jgit.lib.ObjectId; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.Person; import sonia.scm.repository.Repository; -import sonia.scm.repository.util.WorkingCopy; -import sonia.scm.user.User; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import static sonia.scm.ContextEntry.ContextBuilder.entity; -import static sonia.scm.NotFoundException.notFound; - public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand { - private static final Logger LOG = LoggerFactory.getLogger(GitModifyCommand.class); - private final GitWorkdirFactory workdirFactory; - public GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) { + GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) { super(context, repository); this.workdirFactory = workdirFactory; } @Override public String execute(ModifyCommandRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) { - org.eclipse.jgit.lib.Repository repository = workingCopy.getWorkingRepository(); - LOG.debug("cloned repository to folder {}", repository.getWorkTree()); - try { - return new ModifyWorker(repository, request).execute(); - } catch (IOException e) { - return throwInternalRepositoryException("could not apply modifications to cloned repository", e); - } - } + return inClone(clone -> new ModifyWorker(clone, request), workdirFactory); } - private class ModifyWorker implements Worker { + private class ModifyWorker extends GitCloneWorker implements Worker { - private final Git clone; private final File workDir; private final ModifyCommandRequest request; - ModifyWorker(org.eclipse.jgit.lib.Repository repository, ModifyCommandRequest request) { - this.clone = new Git(repository); - this.workDir = repository.getWorkTree(); + ModifyWorker(Git clone, ModifyCommandRequest request) { + super(clone); + this.workDir = clone.getRepository().getWorkTree(); this.request = request; } - String execute() throws IOException { - checkOutBranch(); + @Override + String run() throws IOException { + checkOutBranch(request.getBranch()); for (ModifyCommandRequest.PartialRequest r: request.getRequests()) { r.execute(this); } - doCommit(); + doCommit(request.getCommitMessage(), request.getAuthor()); push(); - return clone.getRepository().getRefDatabase().findRef("HEAD").getObjectId().name(); + return getClone().getRepository().getRefDatabase().findRef("HEAD").getObjectId().name(); } @Override @@ -75,7 +53,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman Files.createDirectories(targetFile.getParent()); Files.copy(file.toPath(), targetFile); try { - clone.add().addFilepattern(toBeCreated).call(); + getClone().add().addFilepattern(toBeCreated).call(); } catch (GitAPIException e) { throwInternalRepositoryException("could not add new file to index", e); } @@ -95,88 +73,6 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman public void move(String sourcePath, String targetPath) { } - - private void checkOutBranch() throws IOException { - String branch = request.getBranch(); - try { - clone.checkout().setName(branch).call(); - } catch (RefNotFoundException e) { - LOG.trace("could not checkout branch {} for modifications directly; trying to create local branch", branch, e); - checkOutTargetAsNewLocalBranch(); - } catch (GitAPIException e) { - throwInternalRepositoryException("could not checkout target branch for merge: " + branch, e); - } - } - - private void checkOutTargetAsNewLocalBranch() throws IOException { - String branch = request.getBranch(); - try { - ObjectId targetRevision = resolveRevision(branch); - clone.checkout().setStartPoint(targetRevision.getName()).setName(branch).setCreateBranch(true).call(); - } catch (RefNotFoundException e) { - LOG.debug("could not checkout branch {} for modifications as local branch", branch, e); - throw notFound(entity("Branch", branch).in(context.getRepository())); - } catch (GitAPIException e) { - throw new InternalRepositoryException(context.getRepository(), "could not checkout branch for modifications as local branch: " + branch, e); - } - } - - private ObjectId resolveRevision(String revision) throws IOException { - ObjectId resolved = clone.getRepository().resolve(revision); - if (resolved == null) { - return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision); - } else { - return resolved; - } - } - - private ObjectId resolveRevisionOrThrowNotFound(org.eclipse.jgit.lib.Repository repository, String revision) throws IOException { - ObjectId resolved = repository.resolve(revision); - if (resolved == null) { - throw notFound(entity("Revision", revision).in(context.getRepository())); - } else { - return resolved; - } - } - - private void doCommit() { - String branch = request.getBranch(); - LOG.debug("modified branch {}", branch); - Person authorToUse = determineAuthor(); - try { - if (!clone.status().call().isClean()) { - clone.commit() - .setAuthor(authorToUse.getName(), authorToUse.getMail()) - .setMessage(request.getCommitMessage()) - .call(); - } - } catch (GitAPIException e) { - throw new InternalRepositoryException(context.getRepository(), "could not commit modifications on branch " + request.getBranch(), e); - } - } - - private Person determineAuthor() { - if (request.getAuthor() == null) { - Subject subject = SecurityUtils.getSubject(); - User user = subject.getPrincipals().oneByType(User.class); - String name = user.getDisplayName(); - String email = user.getMail(); - LOG.debug("no author set; using logged in user: {} <{}>", name, email); - return new Person(name, email); - } else { - return request.getAuthor(); - } - } - - private void push() { - try { - clone.push().call(); - } catch (GitAPIException e) { - throw new IntegrateChangesFromWorkdirException(repository, - "could not push modified branch " + request.getBranch() + " into central repository", e); - } - LOG.debug("pushed modified branch {}", request.getBranch()); - } } private String throwInternalRepositoryException(String message, Exception e) { From 924912b437ebd0b4a067897c1fdbb74e3f8a0dfe Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 29 Aug 2019 13:19:14 +0200 Subject: [PATCH 25/35] Fix preconditions --- .../java/sonia/scm/repository/api/ModifyCommandBuilder.java | 2 +- .../java/sonia/scm/repository/spi/ModifyCommandRequest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 9cfcaebd6a..0dbd45d0a0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -115,7 +115,7 @@ public class ModifyCommandBuilder { */ public String execute() { try { - Preconditions.checkArgument(request.isValid(), "commit message, author and branch are required"); + Preconditions.checkArgument(request.isValid(), "commit message, branch and at least one request are required"); return command.execute(request); } finally { try { diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index 958cd4a485..94bf84ae70 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -65,7 +65,7 @@ public class ModifyCommandRequest implements Resetable, Validateable { @Override public boolean isValid() { - return StringUtils.isNotEmpty(commitMessage) && StringUtils.isNotEmpty(branch) && author != null && !requests.isEmpty(); + return StringUtils.isNotEmpty(commitMessage) && StringUtils.isNotEmpty(branch) && !requests.isEmpty(); } public interface PartialRequest { From 6bd9e0e7dd44d69d186d3bd36e289526b7657245 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 29 Aug 2019 13:47:28 +0200 Subject: [PATCH 26/35] Add messages for IllegalIdentifierChangeException --- scm-webapp/src/main/resources/locales/de/plugins.json | 4 ++++ scm-webapp/src/main/resources/locales/en/plugins.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 92b81c5772..bf534dc138 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -167,6 +167,10 @@ "CHRM7IQzo1": { "displayName": "Änderung fehlgeschlagen", "description": "Die Änderung ist fehlgeschlagen. Bitte wenden Sie sich an ihren Administrator für weitere Hinweise." + }, + "thbsUFokjk": { + "displayName": "Unerlaubte Änderung eines Schlüsselwerts", + "description": "Ein Schlüsselwert wurde unerlaubterweise geändert." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 3f70000624..471da10159 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -167,6 +167,10 @@ "CHRM7IQzo1": { "displayName": "Change failed", "description": "The change failed. Please contact your administrator for further assistance." + }, + "thbsUFokjk": { + "displayName": "Illegal change of an identifier", + "description": "A identifier value has been changed in the entity. This is not allowed." } }, "namespaceStrategies": { From 2674dbaf52580076ee9ec7e418d7961778d675bb Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 29 Aug 2019 15:32:36 +0200 Subject: [PATCH 27/35] suppress UnstableApiUsage and use try with resources --- .../sonia/scm/repository/api/ModifyCommandBuilder.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 9cfcaebd6a..28adca18ae 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -184,17 +184,19 @@ public class ModifyCommandBuilder { } } + @SuppressWarnings("UnstableApiUsage") // Files only used internal private File loadData(ByteSource data) throws IOException { File file = createTemporaryFile(); data.copyTo(Files.asByteSink(file)); return file; } + @SuppressWarnings("UnstableApiUsage") // Files and ByteStreams only used internal private File loadData(InputStream data) throws IOException { File file = createTemporaryFile(); - OutputStream out = Files.asByteSink(file).openBufferedStream(); - ByteStreams.copy(data, out); - out.close(); + try (OutputStream out = Files.asByteSink(file).openBufferedStream()) { + ByteStreams.copy(data, out); + } return file; } From f85460ae62cd200cc6dcf3cbbb7768cd1a744c19 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 29 Aug 2019 13:34:48 +0000 Subject: [PATCH 28/35] Close branch feature/modification_api From 41b27d6e09756097c85329ee11a76890574305ef Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 29 Aug 2019 16:27:40 +0200 Subject: [PATCH 29/35] rename extensionPoints --- scm-ui/src/repos/sources/components/FileTree.js | 2 +- scm-ui/src/repos/sources/components/FileTreeLeaf.js | 4 ++-- scm-ui/src/repos/sources/containers/Content.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scm-ui/src/repos/sources/components/FileTree.js b/scm-ui/src/repos/sources/components/FileTree.js index 2f76b8bce7..d62a2b44db 100644 --- a/scm-ui/src/repos/sources/components/FileTree.js +++ b/scm-ui/src/repos/sources/components/FileTree.js @@ -125,7 +125,7 @@ class FileTree extends React.Component { {t("sources.file-tree.description")} - {binder.hasExtension("sourceView.right") && ( + {binder.hasExtension("repos.sources.tree.row.right") && ( )} diff --git a/scm-ui/src/repos/sources/components/FileTreeLeaf.js b/scm-ui/src/repos/sources/components/FileTreeLeaf.js index 4e9d5008a1..d3c81b960c 100644 --- a/scm-ui/src/repos/sources/components/FileTreeLeaf.js +++ b/scm-ui/src/repos/sources/components/FileTreeLeaf.js @@ -92,11 +92,11 @@ class FileTreeLeaf extends React.Component { > {file.description} - {binder.hasExtension("sourceView.right") && ( + {binder.hasExtension("repos.sources.tree.row.right") && ( {!file.directory && ( diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index d2290913f0..64a0fb472f 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -100,7 +100,7 @@ class Content extends React.Component {
{selector}
From 0e02390019795409eae3d8015ccbfbcbd20dd537 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 29 Aug 2019 14:44:01 +0000 Subject: [PATCH 30/35] Close branch feature/editor_plugin_download From f02dec5cc6e0160fe465e7de4ec70f592ed15c15 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 29 Aug 2019 17:22:50 +0200 Subject: [PATCH 31/35] Add flag to optionally overwrite files with create --- .../repository/api/ModifyCommandBuilder.java | 83 ++++++++++++++----- .../scm/repository/spi/ModifyCommand.java | 2 +- .../repository/spi/ModifyCommandRequest.java | 6 +- .../api/ModifyCommandBuilderTest.java | 40 ++++++++- .../scm/repository/spi/GitModifyCommand.java | 17 +++- .../repository/spi/GitModifyCommandTest.java | 39 ++++++++- 6 files changed, 156 insertions(+), 31 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index bddf9b378a..f5561c839a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -16,12 +16,14 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** * Use this {@link ModifyCommandBuilder} to make file changes to the head of a branch. You can *
    - *
  • create new files ({@link #createFile(String)}
  • + *
  • create new files ({@link #createFile(String)} (with the option to overwrite a file, if it already exists; by + * default a {@link sonia.scm.AlreadyExistsException} will be thrown)
  • *
  • modify existing files ({@link #modifyFile(String)}
  • *
  • delete existing files ({@link #deleteFile(String)}
  • *
  • move/rename existing files ({@link #moveFile(String, String)}
  • @@ -66,24 +68,24 @@ public class ModifyCommandBuilder { /** * Create a new file. The content of the file will be specified in a subsequent call to - * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}. + * {@link SimpleContentLoader#withData(ByteSource)} or {@link SimpleContentLoader#withData(InputStream)}. * @param path The path and the name of the file that should be created. * @return The loader to specify the content of the new file. */ - public ContentLoader createFile(String path) { - return new ContentLoader( - content -> request.addRequest(new ModifyCommandRequest.CreateFileRequest(path, content)) + public WithOverwriteFlagContentLoader createFile(String path) { + return new WithOverwriteFlagContentLoader( + (content, overwrite) -> request.addRequest(new ModifyCommandRequest.CreateFileRequest(path, content, overwrite)) ); } /** * Modify an existing file. The new content of the file will be specified in a subsequent call to - * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}. + * {@link SimpleContentLoader#withData(ByteSource)} or {@link SimpleContentLoader#withData(InputStream)}. * @param path The path and the name of the file that should be modified. * @return The loader to specify the new content of the file. */ - public ContentLoader modifyFile(String path) { - return new ContentLoader( + public SimpleContentLoader modifyFile(String path) { + return new SimpleContentLoader( content -> request.addRequest(new ModifyCommandRequest.ModifyFileRequest(path, content)) ); } @@ -153,30 +155,39 @@ public class ModifyCommandBuilder { return this; } - public class ContentLoader { - - private final Consumer contentConsumer; - - private ContentLoader(Consumer contentConsumer) { - this.contentConsumer = contentConsumer; - } - + public interface ContentLoader { /** * Specify the data of the file using a {@link ByteSource}. + * * @return The builder instance. * @throws IOException If the data could not be read. */ - public ModifyCommandBuilder withData(ByteSource data) throws IOException { - File content = loadData(data); - contentConsumer.accept(content); - return ModifyCommandBuilder.this; - } + ModifyCommandBuilder withData(ByteSource data) throws IOException; /** * Specify the data of the file using an {@link InputStream}. * @return The builder instance. * @throws IOException If the data could not be read. */ + ModifyCommandBuilder withData(InputStream data) throws IOException; + } + + public class SimpleContentLoader implements ContentLoader { + + private final Consumer contentConsumer; + + private SimpleContentLoader(Consumer contentConsumer) { + this.contentConsumer = contentConsumer; + } + + @Override + public ModifyCommandBuilder withData(ByteSource data) throws IOException { + File content = loadData(data); + contentConsumer.accept(content); + return ModifyCommandBuilder.this; + } + + @Override public ModifyCommandBuilder withData(InputStream data) throws IOException { File content = loadData(data); contentConsumer.accept(content); @@ -184,6 +195,36 @@ public class ModifyCommandBuilder { } } + public class WithOverwriteFlagContentLoader implements ContentLoader { + + private final ContentLoader contentLoader; + private boolean overwrite = false; + + private WithOverwriteFlagContentLoader(BiConsumer contentConsumer) { + this.contentLoader = new SimpleContentLoader(file -> contentConsumer.accept(file, overwrite)); + } + + /** + * Set this to true to overwrite the file if it already exists. Otherwise an + * {@link sonia.scm.AlreadyExistsException} will be thrown. + * @return This loader instance. + */ + public WithOverwriteFlagContentLoader setOverwrite(boolean overwrite) { + this.overwrite = overwrite; + return this; + } + + @Override + public ModifyCommandBuilder withData(ByteSource data) throws IOException { + return contentLoader.withData(data); + } + + @Override + public ModifyCommandBuilder withData(InputStream data) throws IOException { + return contentLoader.withData(data); + } + } + @SuppressWarnings("UnstableApiUsage") // Files only used internal private File loadData(ByteSource data) throws IOException { File file = createTemporaryFile(); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java index a8b3efd6d9..578944c47f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommand.java @@ -10,7 +10,7 @@ public interface ModifyCommand { interface Worker { void delete(String toBeDeleted); - void create(String toBeCreated, File file) throws IOException; + void create(String toBeCreated, File file, boolean overwrite) throws IOException; void modify(String path, File file); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index 94bf84ae70..723b287b6e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -124,15 +124,17 @@ public class ModifyCommandRequest implements Resetable, Validateable { public static class CreateFileRequest extends ContentModificationRequest { private final String path; + private final boolean overwrite; - public CreateFileRequest(String path, File content) { + public CreateFileRequest(String path, File content, boolean overwrite) { super(content); this.path = path; + this.overwrite = overwrite; } @Override public void execute(ModifyCommand.Worker worker) throws IOException { - worker.create(path, getContent()); + worker.create(path, getContent(), overwrite); cleanup(); } } diff --git a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java index d88d720cdb..db9a247d65 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/ModifyCommandBuilderTest.java @@ -1,6 +1,7 @@ package sonia.scm.repository.api; import com.google.common.io.ByteSource; +import com.sun.org.apache.xpath.internal.operations.Bool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -25,6 +26,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.lenient; @@ -96,7 +98,7 @@ class ModifyCommandBuilderTest { void shouldExecuteCreateWithByteSourceContent() throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any()); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); initCommand() .createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes())) @@ -110,7 +112,7 @@ class ModifyCommandBuilderTest { void shouldExecuteCreateWithInputStreamContent() throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any()); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); initCommand() .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) @@ -120,11 +122,43 @@ class ModifyCommandBuilderTest { assertThat(contentCaptor).contains("content"); } + @Test + void shouldExecuteCreateWithOverwriteFalseAsDefault() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture()); + + initCommand() + .createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(overwriteCaptor.getValue()).isFalse(); + assertThat(contentCaptor).contains("content"); + } + + @Test + void shouldExecuteCreateWithOverwriteIfSet() throws IOException { + ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor overwriteCaptor = ArgumentCaptor.forClass(Boolean.class); + List contentCaptor = new ArrayList<>(); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture()); + + initCommand() + .createFile("toBeCreated").setOverwrite(true).withData(new ByteArrayInputStream("content".getBytes())) + .execute(); + + assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated"); + assertThat(overwriteCaptor.getValue()).isTrue(); + assertThat(contentCaptor).contains("content"); + } + @Test void shouldExecuteCreateMultipleTimes() throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); List contentCaptor = new ArrayList<>(); - doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any()); + doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean()); initCommand() .createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes())) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index 8825645a3c..2c473398c5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -8,9 +8,14 @@ import sonia.scm.repository.Repository; import java.io.File; import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static sonia.scm.AlreadyExistsException.alreadyExists; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand { private final GitWorkdirFactory workdirFactory; @@ -48,10 +53,18 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman } @Override - public void create(String toBeCreated, File file) throws IOException { + public void create(String toBeCreated, File file, boolean overwrite) throws IOException { Path targetFile = new File(workDir, toBeCreated).toPath(); Files.createDirectories(targetFile.getParent()); - Files.copy(file.toPath(), targetFile); + if (overwrite) { + Files.copy(file.toPath(), targetFile, REPLACE_EXISTING); + } else { + try { + Files.copy(file.toPath(), targetFile); + } catch (FileAlreadyExistsException e) { + throw alreadyExists(entity("file", toBeCreated).in("branch", request.getBranch()).in(context.getRepository())); + } + } try { getClone().add().addFilepattern(toBeCreated).call(); } catch (GitAPIException e) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index e8574b7baf..2d9f5d0235 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -13,6 +13,7 @@ import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import sonia.scm.AlreadyExistsException; import sonia.scm.repository.Person; import sonia.scm.repository.util.WorkdirProvider; @@ -41,7 +42,7 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { ModifyCommandRequest request = new ModifyCommandRequest(); request.setBranch("master"); request.setCommitMessage("test commit"); - request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile)); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); String newRef = command.execute(request); @@ -63,7 +64,7 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { ModifyCommandRequest request = new ModifyCommandRequest(); request.setBranch("master"); request.setCommitMessage("test commit"); - request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile)); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); command.execute(request); @@ -73,6 +74,40 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { assertInTree(assertions); } + @Test(expected = AlreadyExistsException.class) + public void shouldFailIfOverwritingExistingFileWithoutOverwriteFlag() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setBranch("master"); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + + @Test + public void shouldOverwriteExistingFileIfOverwriteFlagSet() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setBranch("master"); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", newFile, true)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("a.txt")).isTrue(); + + assertInTree(assertions); + } + private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { try (Git git = new Git(createContext().open())) { RevCommit lastCommit = getLastCommit(git); From bdc2622df81a25dbb5b0e9c4a796d88474573480 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 29 Aug 2019 17:30:33 +0200 Subject: [PATCH 32/35] Fix javadoc --- .../sonia/scm/repository/api/ModifyCommandBuilder.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index f5561c839a..5baa7dcd26 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -68,7 +68,10 @@ public class ModifyCommandBuilder { /** * Create a new file. The content of the file will be specified in a subsequent call to - * {@link SimpleContentLoader#withData(ByteSource)} or {@link SimpleContentLoader#withData(InputStream)}. + * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}. + * By default, an {@link sonia.scm.AlreadyExistsException} will be thrown, when there already + * exists a file with the given path. You can disable this setting + * {@link WithOverwriteFlagContentLoader#setOverwrite(boolean)} to true. * @param path The path and the name of the file that should be created. * @return The loader to specify the content of the new file. */ @@ -80,7 +83,7 @@ public class ModifyCommandBuilder { /** * Modify an existing file. The new content of the file will be specified in a subsequent call to - * {@link SimpleContentLoader#withData(ByteSource)} or {@link SimpleContentLoader#withData(InputStream)}. + * {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}. * @param path The path and the name of the file that should be modified. * @return The loader to specify the new content of the file. */ From 8a7b50a079fa019e9e9b556439451bfff0b711f2 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 29 Aug 2019 17:57:29 +0200 Subject: [PATCH 33/35] Throw exception when no changes were made --- .../repository/spi/AbstractGitCommand.java | 22 ++++++++++++++++--- .../scm/repository/spi/GitModifyCommand.java | 16 +++++++++++++- .../repository/spi/GitModifyCommandTest.java | 16 ++++++++++++++ .../main/resources/locales/de/plugins.json | 4 ++++ .../main/resources/locales/en/plugins.json | 4 ++++ 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index d8c0ff1c60..6066b66371 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -43,6 +43,7 @@ import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitUtil; @@ -55,7 +56,10 @@ import sonia.scm.user.User; import java.io.IOException; import java.util.Optional; import java.util.function.Function; +import java.util.function.Supplier; +import static java.util.Optional.empty; +import static java.util.Optional.of; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -200,14 +204,26 @@ class AbstractGitCommand } } - void doCommit(String message, Person author) { + void failIfNotChanged(Supplier doThrow) { + try { + if (clone.status().call().isClean()) { + throw doThrow.get(); + } + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not read status of repository", e); + } + } + + Optional doCommit(String message, Person author) { Person authorToUse = determineAuthor(author); try { if (!clone.status().call().isClean()) { - clone.commit() + return of(clone.commit() .setAuthor(authorToUse.getName(), authorToUse.getMail()) .setMessage(message) - .call(); + .call()); + } else { + return empty(); } } catch (GitAPIException e) { throw new InternalRepositoryException(context.getRepository(), "could not commit changes", e); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index 2c473398c5..c86c94aea1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -2,6 +2,8 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; +import sonia.scm.BadRequestException; +import sonia.scm.ContextEntry; import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; @@ -44,9 +46,10 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman @Override String run() throws IOException { checkOutBranch(request.getBranch()); - for (ModifyCommandRequest.PartialRequest r: request.getRequests()) { + for (ModifyCommandRequest.PartialRequest r : request.getRequests()) { r.execute(this); } + failIfNotChanged(NoChangesMadeException::new); doCommit(request.getCommitMessage(), request.getAuthor()); push(); return getClone().getRepository().getRefDatabase().findRef("HEAD").getObjectId().name(); @@ -86,6 +89,17 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman public void move(String sourcePath, String targetPath) { } + + private class NoChangesMadeException extends BadRequestException { + public NoChangesMadeException() { + super(ContextEntry.ContextBuilder.entity(context.getRepository()).build(), "no changes detected to branch " + ModifyWorker.this.request.getBranch()); + } + + @Override + public String getCode() { + return "40RaYIeeR1"; + } + } } private String throwInternalRepositoryException(String message, Exception e) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index 2d9f5d0235..c1e1c05631 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -14,6 +14,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import sonia.scm.AlreadyExistsException; +import sonia.scm.BadRequestException; import sonia.scm.repository.Person; import sonia.scm.repository.util.WorkdirProvider; @@ -108,6 +109,21 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { assertInTree(assertions); } + @Test(expected = BadRequestException.class) + public void shouldFailIfNoChangesMade() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "b\n".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setBranch("master"); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("b.txt", newFile, true)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + } + private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { try (Git git = new Git(createContext().open())) { RevCommit lastCommit = getLastCommit(git); diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index bf534dc138..9c5bb3f5f5 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -171,6 +171,10 @@ "thbsUFokjk": { "displayName": "Unerlaubte Änderung eines Schlüsselwerts", "description": "Ein Schlüsselwert wurde unerlaubterweise geändert." + }, + "40RaYIeeR1": { + "displayName": "Es wurden keine Änderungen durchgeführt", + "description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt werden." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 471da10159..1ffffd73b7 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -171,6 +171,10 @@ "thbsUFokjk": { "displayName": "Illegal change of an identifier", "description": "A identifier value has been changed in the entity. This is not allowed." + }, + "40RaYIeeR1": { + "displayName": "No changes were made", + "description": "No changes were made to the files of the repository. Therefor no new commit could be created." } }, "namespaceStrategies": { From 8ccd99975f7fa627ed6745e708ad2b6c50e54ec4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 29 Aug 2019 17:59:34 +0200 Subject: [PATCH 34/35] Use revision from commit as result --- .../java/sonia/scm/repository/spi/GitModifyCommand.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index c86c94aea1..680ef153bd 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -2,6 +2,7 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.revwalk.RevCommit; import sonia.scm.BadRequestException; import sonia.scm.ContextEntry; import sonia.scm.repository.GitWorkdirFactory; @@ -13,6 +14,7 @@ import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static sonia.scm.AlreadyExistsException.alreadyExists; @@ -50,9 +52,9 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman r.execute(this); } failIfNotChanged(NoChangesMadeException::new); - doCommit(request.getCommitMessage(), request.getAuthor()); + Optional revCommit = doCommit(request.getCommitMessage(), request.getAuthor()); push(); - return getClone().getRepository().getRefDatabase().findRef("HEAD").getObjectId().name(); + return revCommit.orElseThrow(NoChangesMadeException::new).name(); } @Override From 86db0b4565990b7df9dafa4e6f4f0bbda7183828 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 30 Aug 2019 05:51:20 +0000 Subject: [PATCH 35/35] Close branch feature/git_modification_api