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