diff --git a/pom.xml b/pom.xml index 3ab3cb82a3..c14ce8b39b 100644 --- a/pom.xml +++ b/pom.xml @@ -843,7 +843,7 @@ 1.4.0 - v5.4.0.201906121030-r-scm1 + v5.4.0.201906121030-r-scm2 1.9.0-scm3 diff --git a/scm-core/src/main/java/sonia/scm/NoChangesMadeException.java b/scm-core/src/main/java/sonia/scm/NoChangesMadeException.java new file mode 100644 index 0000000000..9bf0363398 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/NoChangesMadeException.java @@ -0,0 +1,18 @@ +package sonia.scm; + +import sonia.scm.repository.Repository; + +public class NoChangesMadeException extends BadRequestException { + public NoChangesMadeException(Repository repository, String branch) { + super(ContextEntry.ContextBuilder.entity(repository).build(), "no changes detected to branch " + branch); + } + + public NoChangesMadeException(Repository repository) { + super(ContextEntry.ContextBuilder.entity(repository).build(), "no changes detected"); + } + + @Override + public String getCode() { + return "40RaYIeeR1"; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java index 8fcfc937e5..0a2267e888 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java @@ -4,6 +4,7 @@ import com.google.common.base.Preconditions; import sonia.scm.repository.Person; import sonia.scm.repository.spi.MergeCommand; import sonia.scm.repository.spi.MergeCommandRequest; +import sonia.scm.repository.util.AuthorUtil; /** * Use this {@link MergeCommandBuilder} to merge two branches of a repository ({@link #executeMerge()}) or to check if @@ -126,6 +127,7 @@ public class MergeCommandBuilder { * @return The result of the merge. */ public MergeCommandResult executeMerge() { + AuthorUtil.setAuthorIfNotAvailable(request); Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required"); return mergeCommand.merge(request); } 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 1eed0697cb..05c1babe03 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 @@ -9,6 +9,7 @@ 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.AuthorUtil; import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.util.IOUtil; @@ -94,22 +95,12 @@ public class ModifyCommandBuilder { return this; } - /** - * 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; - } - /** * Apply the changes and create a new commit with the given message and author. * @return The revision of the new commit. */ public String execute() { + AuthorUtil.setAuthorIfNotAvailable(request); try { Preconditions.checkArgument(request.isValid(), "commit message, branch and at least one request are required"); return command.execute(request); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java index 45dfc0b2b7..223cf8c49e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java @@ -5,10 +5,11 @@ import com.google.common.base.Objects; import com.google.common.base.Strings; import sonia.scm.Validateable; import sonia.scm.repository.Person; +import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor; import java.io.Serializable; -public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable { +public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable, CommandWithAuthor { private static final long serialVersionUID = -2650236557922431528L; 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 b3ba882040..1bc64e0e08 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 @@ -13,7 +13,5 @@ public interface ModifyCommand { void create(String toBeCreated, File file, boolean overwrite) throws IOException; void modify(String path, File file) throws IOException; - - 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 index d61e17c785..43814f8729 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.Validateable; import sonia.scm.repository.Person; +import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor; import sonia.scm.util.IOUtil; import java.io.File; @@ -13,7 +14,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class ModifyCommandRequest implements Resetable, Validateable { +public class ModifyCommandRequest implements Resetable, Validateable, CommandWithAuthor { private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandRequest.class); @@ -94,21 +95,6 @@ public class ModifyCommandRequest implements Resetable, Validateable { } } - 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; diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java new file mode 100644 index 0000000000..a1ff6b0eb3 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyWorkerHelper.java @@ -0,0 +1,88 @@ +package sonia.scm.repository.spi; + +import org.apache.commons.lang.StringUtils; +import sonia.scm.ContextEntry; +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.NoSuchFileException; +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; +import static sonia.scm.NotFoundException.notFound; + +/** + * This "interface" is not really intended to be used as an interface but rather as + * a base class to reduce code redundancy in Worker instances. + */ +public interface ModifyWorkerHelper extends ModifyCommand.Worker { + + @Override + default void delete(String toBeDeleted) throws IOException { + Path fileToBeDeleted = new File(getWorkDir(), toBeDeleted).toPath(); + try { + Files.delete(fileToBeDeleted); + } catch (NoSuchFileException e) { + throw notFound(createFileContext(toBeDeleted)); + } + doScmDelete(toBeDeleted); + } + + void doScmDelete(String toBeDeleted); + + @Override + default void create(String toBeCreated, File file, boolean overwrite) throws IOException { + Path targetFile = new File(getWorkDir(), toBeCreated).toPath(); + createDirectories(targetFile); + if (overwrite) { + Files.move(file.toPath(), targetFile, REPLACE_EXISTING); + } else { + try { + Files.move(file.toPath(), targetFile); + } catch (FileAlreadyExistsException e) { + throw alreadyExists(createFileContext(toBeCreated)); + } + } + addFileToScm(toBeCreated, targetFile); + } + + default void modify(String path, File file) throws IOException { + Path targetFile = new File(getWorkDir(), path).toPath(); + createDirectories(targetFile); + if (!targetFile.toFile().exists()) { + throw notFound(createFileContext(path)); + } + Files.move(file.toPath(), targetFile, REPLACE_EXISTING); + addFileToScm(path, targetFile); + } + + void addFileToScm(String name, Path file); + + default ContextEntry.ContextBuilder createFileContext(String path) { + ContextEntry.ContextBuilder contextBuilder = entity("file", path); + if (!StringUtils.isEmpty(getBranch())) { + contextBuilder.in("branch", getBranch()); + } + contextBuilder.in(getRepository()); + return contextBuilder; + } + + default void createDirectories(Path targetFile) throws IOException { + try { + Files.createDirectories(targetFile.getParent()); + } catch (FileAlreadyExistsException e) { + throw alreadyExists(createFileContext(targetFile.toString())); + } + } + + File getWorkDir(); + + Repository getRepository(); + + String getBranch(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java b/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java new file mode 100644 index 0000000000..5735889c3c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/AuthorUtil.java @@ -0,0 +1,29 @@ +package sonia.scm.repository.util; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; +import sonia.scm.repository.Person; +import sonia.scm.user.User; + +public class AuthorUtil { + + public static void setAuthorIfNotAvailable(CommandWithAuthor request) { + if (request.getAuthor() == null) { + request.setAuthor(createAuthorFromSubject()); + } + } + + private static Person createAuthorFromSubject() { + Subject subject = SecurityUtils.getSubject(); + User user = subject.getPrincipals().oneByType(User.class); + String name = user.getDisplayName(); + String email = user.getMail(); + return new Person(name, email); + } + + public interface CommandWithAuthor { + Person getAuthor(); + + void setAuthor(Person person); + } +} 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 db9a247d65..6e46841cd2 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,7 +1,6 @@ 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; @@ -85,15 +84,6 @@ class ModifyCommandBuilderTest { verify(worker).delete("toBeDeleted"); } - @Test - void shouldExecuteMove() throws IOException { - initCommand() - .moveFile("source", "target") - .execute(); - - verify(worker).move("source", "target"); - } - @Test void shouldExecuteCreateWithByteSourceContent() throws IOException { ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLfsFilterContextListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLfsFilterContextListener.java new file mode 100644 index 0000000000..c202874781 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLfsFilterContextListener.java @@ -0,0 +1,77 @@ +package sonia.scm.repository.spi; + +import com.google.common.io.ByteStreams; +import org.eclipse.jgit.attributes.FilterCommand; +import org.eclipse.jgit.attributes.FilterCommandRegistry; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Pattern; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +@Extension +public class GitLfsFilterContextListener implements ServletContextListener { + + public static final String GITCONFIG = "[filter \"lfs\"]\n" + + "clean = git-lfs clean -- %f\n" + + "smudge = git-lfs smudge -- %f\n" + + "process = git-lfs filter-process\n" + + "required = true\n"; + public static final Pattern COMMAND_NAME_PATTERN = Pattern.compile("git-lfs (smudge|clean) -- .*"); + + private static final Logger LOG = LoggerFactory.getLogger(GitLfsFilterContextListener.class); + + private final SCMContextProvider contextProvider; + + @Inject + public GitLfsFilterContextListener(SCMContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + Path gitconfig = contextProvider.getBaseDirectory().toPath().resolve("gitconfig"); + try { + Files.write(gitconfig, GITCONFIG.getBytes(Charset.defaultCharset()), TRUNCATE_EXISTING, CREATE); + FS.DETECTED.setGitSystemConfig(gitconfig.toFile()); + LOG.info("wrote git config file: {}", gitconfig); + } catch (IOException e) { + LOG.error("could not write git config in path {}; git lfs support may not work correctly", gitconfig, e); + } + FilterCommandRegistry.register(COMMAND_NAME_PATTERN, NoOpFilterCommand::new); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + FilterCommandRegistry.unregister(COMMAND_NAME_PATTERN); + } + + private static class NoOpFilterCommand extends FilterCommand { + NoOpFilterCommand(Repository db, InputStream in, OutputStream out) { + super(in, out); + } + + @Override + public int run() throws IOException { + ByteStreams.copy(in, out); + in.close(); + out.close(); + return -1; + } + } +} 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 3a8ec03a08..a68be0a4da 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,36 +1,38 @@ package sonia.scm.repository.spi; +import com.google.common.util.concurrent.Striped; import org.apache.commons.lang.StringUtils; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.attributes.FilterCommandRegistry; import org.eclipse.jgit.revwalk.RevCommit; -import sonia.scm.BadRequestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.ConcurrentModificationException; -import sonia.scm.ContextEntry; +import sonia.scm.NoChangesMadeException; import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.File; import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Optional; - -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -import static sonia.scm.AlreadyExistsException.alreadyExists; -import static sonia.scm.ContextEntry.ContextBuilder.entity; -import static sonia.scm.NotFoundException.notFound; +import java.util.concurrent.locks.Lock; public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand { - private final GitWorkdirFactory workdirFactory; + private static final Logger LOG = LoggerFactory.getLogger(GitModifyCommand.class); + private static final Striped REGISTER_LOCKS = Striped.lock(5); - GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) { + private final GitWorkdirFactory workdirFactory; + private final LfsBlobStoreFactory lfsBlobStoreFactory; + + GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context, repository); this.workdirFactory = workdirFactory; + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } @Override @@ -38,7 +40,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman return inClone(clone -> new ModifyWorker(clone, request), workdirFactory, request.getBranch()); } - private class ModifyWorker extends GitCloneWorker implements Worker { + private class ModifyWorker extends GitCloneWorker implements ModifyWorkerHelper { private final File workDir; private final ModifyCommandRequest request; @@ -52,52 +54,42 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman @Override String run() throws IOException { getClone().getRepository().getFullBranch(); - if (!StringUtils.isEmpty(request.getExpectedRevision())) { - if (!request.getExpectedRevision().equals(getCurrentRevision().getName())) { - throw new ConcurrentModificationException("branch", request.getBranch() == null? "default": request.getBranch()); - } + if (!StringUtils.isEmpty(request.getExpectedRevision()) + && !request.getExpectedRevision().equals(getCurrentRevision().getName())) { + throw new ConcurrentModificationException("branch", request.getBranch() == null ? "default" : request.getBranch()); } for (ModifyCommandRequest.PartialRequest r : request.getRequests()) { r.execute(this); } - failIfNotChanged(NoChangesMadeException::new); + failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())); Optional revCommit = doCommit(request.getCommitMessage(), request.getAuthor()); push(); - return revCommit.orElseThrow(NoChangesMadeException::new).name(); + return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name(); } @Override - public void create(String toBeCreated, File file, boolean overwrite) throws IOException { - Path targetFile = new File(workDir, toBeCreated).toPath(); - createDirectories(targetFile); - if (overwrite) { - Files.move(file.toPath(), targetFile, REPLACE_EXISTING); - } else { + public void addFileToScm(String name, Path file) { + addToGitWithLfsSupport(name, file); + } + + private void addToGitWithLfsSupport(String path, Path targetFile) { + REGISTER_LOCKS.get(targetFile).lock(); + try { + LfsBlobStoreCleanFilterFactory cleanFilterFactory = new LfsBlobStoreCleanFilterFactory(lfsBlobStoreFactory, repository, targetFile); + + String registerKey = "git-lfs clean -- '" + path + "'"; + LOG.debug("register lfs filter command factory for command '{}'", registerKey); + FilterCommandRegistry.register(registerKey, cleanFilterFactory::createFilter); try { - Files.move(file.toPath(), targetFile); - } catch (FileAlreadyExistsException e) { - throw alreadyExists(createFileContext(toBeCreated)); + addFileToGit(path); + } catch (GitAPIException e) { + throwInternalRepositoryException("could not add file to index", e); + } finally { + LOG.debug("unregister lfs filter command factory for command \"{}\"", registerKey); + FilterCommandRegistry.unregister(registerKey); } - } - try { - addFileToGit(toBeCreated); - } catch (GitAPIException e) { - throwInternalRepositoryException("could not add new file to index", e); - } - } - - @Override - public void modify(String path, File file) throws IOException { - Path targetFile = new File(workDir, path).toPath(); - createDirectories(targetFile); - if (!targetFile.toFile().exists()) { - throw notFound(createFileContext(path)); - } - Files.move(file.toPath(), targetFile, REPLACE_EXISTING); - try { - addFileToGit(path); - } catch (GitAPIException e) { - throwInternalRepositoryException("could not add new file to index", e); + } finally { + REGISTER_LOCKS.get(targetFile).unlock(); } } @@ -106,13 +98,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman } @Override - public void delete(String toBeDeleted) throws IOException { - Path fileToBeDeleted = new File(workDir, toBeDeleted).toPath(); - try { - Files.delete(fileToBeDeleted); - } catch (NoSuchFileException e) { - throw notFound(createFileContext(toBeDeleted)); - } + public void doScmDelete(String toBeDeleted) { try { getClone().rm().addFilepattern(removeStartingPathSeparators(toBeDeleted)).call(); } catch (GitAPIException e) { @@ -120,6 +106,21 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman } } + @Override + public File getWorkDir() { + return workDir; + } + + @Override + public Repository getRepository() { + return repository; + } + + @Override + public String getBranch() { + return request.getBranch(); + } + private String removeStartingPathSeparators(String path) { while (path.startsWith(File.separator)) { path = path.substring(1); @@ -127,41 +128,8 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman return path; } - private void createDirectories(Path targetFile) throws IOException { - try { - Files.createDirectories(targetFile.getParent()); - } catch (FileAlreadyExistsException e) { - throw alreadyExists(createFileContext(targetFile.toString())); - } + private String throwInternalRepositoryException(String message, Exception e) { + throw new InternalRepositoryException(context.getRepository(), message, e); } - - private ContextEntry.ContextBuilder createFileContext(String path) { - ContextEntry.ContextBuilder contextBuilder = entity("file", path); - if (!StringUtils.isEmpty(request.getBranch())) { - contextBuilder.in("branch", request.getBranch()); - } - contextBuilder.in(context.getRepository()); - return contextBuilder; - } - - @Override - 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) { - 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 4c02a3a73c..dc43b8d9b7 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 @@ -273,7 +273,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public ModifyCommand getModifyCommand() { - return new GitModifyCommand(context, repository, handler.getWorkdirFactory()); + return new GitModifyCommand(context, repository, handler.getWorkdirFactory(), lfsBlobStoreFactory); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilter.java new file mode 100644 index 0000000000..bb54ea162a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilter.java @@ -0,0 +1,83 @@ +package sonia.scm.repository.spi; + +import com.google.common.io.ByteStreams; +import org.eclipse.jgit.attributes.FilterCommand; +import org.eclipse.jgit.lfs.LfsPointer; +import org.eclipse.jgit.lfs.lib.AnyLongObjectId; +import org.eclipse.jgit.lfs.lib.Constants; +import org.eclipse.jgit.lfs.lib.LongObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; +import sonia.scm.util.IOUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestOutputStream; + +/** + * Adapted version of JGit's {@link org.eclipse.jgit.lfs.CleanFilter} to write the + * lfs file directly to the lfs blob store. + */ +class LfsBlobStoreCleanFilter extends FilterCommand { + + private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreCleanFilter.class); + + private final BlobStore lfsBlobStore; + private final Path targetFile; + + LfsBlobStoreCleanFilter(InputStream in, OutputStream out, BlobStore lfsBlobStore, Path targetFile) { + super(in, out); + this.lfsBlobStore = lfsBlobStore; + this.targetFile = targetFile; + } + + @Override + // Suppress warning for RuntimeException after check for wrong size, because mathematicians say this will never happen + @SuppressWarnings("squid:S00112") + public int run() throws IOException { + LOG.debug("running scm lfs filter for file {}", targetFile); + DigestOutputStream digestOutputStream = createDigestStream(); + try { + long size = ByteStreams.copy(in, digestOutputStream); + AnyLongObjectId loid = LongObjectId.fromRaw(digestOutputStream.getMessageDigest().digest()); + String hash = loid.getName(); + + Blob existingBlob = lfsBlobStore.get(hash); + if (existingBlob != null) { + LOG.debug("found existing lfs blob for oid {}", hash); + long blobSize = existingBlob.getSize(); + if (blobSize != size) { + throw new RuntimeException("lfs entry already exists for loid " + hash + " but has wrong size"); + } + } else { + LOG.debug("uploading new lfs blob for oid {}", hash); + Blob newBlob = lfsBlobStore.create(hash); + OutputStream outputStream = newBlob.getOutputStream(); + Files.copy(targetFile, outputStream); + newBlob.commit(); + } + + LfsPointer lfsPointer = new LfsPointer(loid, size); + lfsPointer.encode(out); + return -1; + } finally { + IOUtil.close(digestOutputStream); + IOUtil.close(in); + IOUtil.close(out); + } + } + + private DigestOutputStream createDigestStream() { + return new DigestOutputStream(new OutputStream() { + @Override + public void write(int b) { + // no further target here, we are just interested in the digest + } + }, Constants.newMessageDigest()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilterFactory.java new file mode 100644 index 0000000000..817d763d10 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/LfsBlobStoreCleanFilterFactory.java @@ -0,0 +1,27 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.lib.Repository; +import sonia.scm.web.lfs.LfsBlobStoreFactory; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Path; + +class LfsBlobStoreCleanFilterFactory { + + private final LfsBlobStoreFactory blobStoreFactory; + private final sonia.scm.repository.Repository repository; + private final Path targetFile; + + LfsBlobStoreCleanFilterFactory(LfsBlobStoreFactory blobStoreFactory, sonia.scm.repository.Repository repository, Path targetFile) { + this.blobStoreFactory = blobStoreFactory; + this.repository = repository; + this.targetFile = targetFile; + } + + @SuppressWarnings("squid:S1172") + // suppress unused parameter to keep the api compatible to jgit's FilterCommandFactory + LfsBlobStoreCleanFilter createFilter(Repository db, InputStream in, OutputStream out) { + return new LfsBlobStoreCleanFilter(in, out, blobStoreFactory.getLfsBlobStore(repository), targetFile); + } +} 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 501134026d..37d0202d41 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 @@ -17,15 +17,16 @@ import sonia.scm.AlreadyExistsException; import sonia.scm.BadRequestException; import sonia.scm.ConcurrentModificationException; import sonia.scm.NotFoundException; -import sonia.scm.ScmConstraintViolationException; import sonia.scm.repository.Person; import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.File; import java.io.IOException; import java.nio.file.Files; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; @SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") public class GitModifyCommandTest extends AbstractGitCommandTestBase { @@ -37,6 +38,8 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { @Rule public ShiroRule shiro = new ShiroRule(); + private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + @Test public void shouldCreateCommit() throws IOException, GitAPIException { File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); @@ -296,7 +299,7 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { } private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory); } @FunctionalInterface diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java new file mode 100644 index 0000000000..b1a5c7bbcc --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java @@ -0,0 +1,116 @@ +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.revwalk.RevCommit; +import org.junit.After; +import org.junit.Before; +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 sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; +import sonia.scm.web.lfs.LfsBlobStoreFactory; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +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 GitModifyCommand_LFSTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + @Rule + public ShiroRule shiro = new ShiroRule(); + + private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + + @Before + public void registerFilter() { + new GitLfsFilterContextListener(contextProvider).contextInitialized(null); + } + + @After + public void unregisterFilter() { + new GitLfsFilterContextListener(contextProvider).contextDestroyed(null); + } + + @Test + public void shouldCreateCommit() throws IOException, GitAPIException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String newRef = createCommit("new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", outputStream); + + 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()); + } + + assertThat(outputStream.toString()).isEqualTo("new content"); + } + + @Test + public void shouldCreateSecondCommits() throws IOException, GitAPIException { + createCommit("new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", new ByteArrayOutputStream()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String newRef = createCommit("more_lfs.png", "more content", "2c2316737c9313956dfc0083da3a2a62ce259f66484f3e26440f0d1b02dd4128", outputStream); + + 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()); + } + + assertThat(outputStream.toString()).isEqualTo("more content"); + } + + private String createCommit(String fileName, String content, String hashOfContent, ByteArrayOutputStream outputStream) throws IOException { + BlobStore blobStore = mock(BlobStore.class); + Blob blob = mock(Blob.class); + when(lfsBlobStoreFactory.getLfsBlobStore(any())).thenReturn(blobStore); + when(blobStore.create(hashOfContent)).thenReturn(blob); + when(blobStore.get(hashOfContent)).thenReturn(null, blob); + when(blob.getOutputStream()).thenReturn(outputStream); + when(blob.getSize()).thenReturn((long) content.length()); + + File newFile = Files.write(temporaryFolder.newFile().toPath(), content.getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest(fileName, newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + return command.execute(request); + } + + 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()), lfsBlobStoreFactory); + } + + @Override + protected String getZippedRepositoryResource() { + return "sonia/scm/repository/spi/scm-git-spi-lfs-test.zip"; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java index ff409e470d..443eedb14a 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java @@ -13,15 +13,16 @@ import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import sonia.scm.ScmConstraintViolationException; import sonia.scm.repository.Person; import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.File; import java.io.IOException; import java.nio.file.Files; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; @SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommandTestBase { @@ -33,6 +34,8 @@ public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommand @Rule public ShiroRule shiro = new ShiroRule(); + private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + @Test public void shouldCreateNewFileInEmptyRepository() throws IOException, GitAPIException { File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); @@ -74,7 +77,7 @@ public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommand } private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory); } @FunctionalInterface diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip new file mode 100644 index 0000000000..b97f519684 Binary files /dev/null and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip differ diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java new file mode 100644 index 0000000000..0294b6902b --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java @@ -0,0 +1,103 @@ +package sonia.scm.repository.spi; + +import com.aragost.javahg.Changeset; +import com.aragost.javahg.Repository; +import com.aragost.javahg.commands.CommitCommand; +import com.aragost.javahg.commands.ExecutionException; +import com.aragost.javahg.commands.PullCommand; +import com.aragost.javahg.commands.RemoveCommand; +import com.aragost.javahg.commands.StatusCommand; +import sonia.scm.NoChangesMadeException; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public class HgModifyCommand implements ModifyCommand { + + private HgCommandContext context; + private final HgWorkdirFactory workdirFactory; + + public HgModifyCommand(HgCommandContext context, HgWorkdirFactory workdirFactory) { + this.context = context; + this.workdirFactory = workdirFactory; + } + + @Override + public String execute(ModifyCommandRequest request) { + + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, request.getBranch())) { + Repository workingRepository = workingCopy.getWorkingRepository(); + request.getRequests().forEach( + partialRequest -> { + try { + partialRequest.execute(new ModifyWorkerHelper() { + + @Override + public void addFileToScm(String name, Path file) { + try { + addFileToHg(file.toFile()); + } catch (ExecutionException e) { + throwInternalRepositoryException("could not add new file to index", e); + } + } + + @Override + public void doScmDelete(String toBeDeleted) { + RemoveCommand.on(workingRepository).execute(toBeDeleted); + } + + @Override + public sonia.scm.repository.Repository getRepository() { + return context.getScmRepository(); + } + + @Override + public String getBranch() { + return request.getBranch(); + } + + public File getWorkDir() { + return workingRepository.getDirectory(); + } + + private void addFileToHg(File file) { + workingRepository.workingCopy().add(file.getAbsolutePath()); + } + }); + } catch (IOException e) { + throwInternalRepositoryException("could not execute command on repository", e); + } + } + ); + if (StatusCommand.on(workingRepository).lines().isEmpty()) { + throw new NoChangesMadeException(context.getScmRepository()); + } + CommitCommand.on(workingRepository).user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail())).message(request.getCommitMessage()).execute(); + List execute = pullModifyChangesToCentralRepository(request, workingCopy); + return execute.get(0).getNode(); + } catch (ExecutionException e) { + throwInternalRepositoryException("could not execute command on repository", e); + return null; + } + } + + private List pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy workingCopy) { + try { + com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); + workdirFactory.configure(pullCommand); + return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath()); + } catch (Exception e) { + throw new IntegrateChangesFromWorkdirException(context.getScmRepository(), + String.format("Could not pull modify changes from working copy to central repository for branch %s", request.getBranch()), + e); + } + } + + private String throwInternalRepositoryException(String message, Exception e) { + throw new InternalRepositoryException(context.getScmRepository(), message, e); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index d60e888cac..c80699add8 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -66,7 +66,8 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider Command.INCOMING, Command.OUTGOING, Command.PUSH, - Command.PULL + Command.PULL, + Command.MODIFY ); //J+ @@ -77,7 +78,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- HgRepositoryServiceProvider(HgRepositoryHandler handler, - HgHookManager hookManager, Repository repository) + HgHookManager hookManager, Repository repository) { this.repository = repository; this.handler = handler; @@ -238,6 +239,11 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider return new HgPushCommand(handler, context, repository); } + @Override + public ModifyCommand getModifyCommand() { + return new HgModifyCommand(context, handler.getWorkdirFactory()); + } + /** * Method description * diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java index d6d04ee017..7519cb564d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java @@ -47,12 +47,12 @@ import sonia.scm.repository.Repository; public class HgRepositoryServiceResolver implements RepositoryServiceResolver { - private HgRepositoryHandler handler; - private HgHookManager hookManager; + private final HgRepositoryHandler handler; + private final HgHookManager hookManager; @Inject public HgRepositoryServiceResolver(HgRepositoryHandler handler, - HgHookManager hookManager) + HgHookManager hookManager) { this.handler = handler; this.hookManager = hookManager; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java new file mode 100644 index 0000000000..862d2ab2ed --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java @@ -0,0 +1,164 @@ +package sonia.scm.repository.spi; + +import com.google.inject.util.Providers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.AlreadyExistsException; +import sonia.scm.NoChangesMadeException; +import sonia.scm.NotFoundException; +import sonia.scm.repository.HgHookManager; +import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.HgRepositoryEnvironmentBuilder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HgModifyCommandTest extends AbstractHgCommandTestBase { + + private HgModifyCommand hgModifyCommand; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void initHgModifyCommand() { + HgHookManager hookManager = HgTestUtil.createHookManager(); + HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager); + SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(environmentBuilder), new WorkdirProvider()) { + @Override + public void configure(com.aragost.javahg.commands.PullCommand pullCommand) { + // we do not want to configure http hooks in this unit test + } + }; + hgModifyCommand = new HgModifyCommand(cmdContext, workdirFactory + ); + } + + @Test + public void shouldRemoveFiles() { + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + String result = hgModifyCommand.execute(request); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(result); + } + + @Test + public void shouldCreateFilesWithoutOverwrite() throws IOException { + File testFile = temporaryFolder.newFile(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + String changeSet = hgModifyCommand.execute(request); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet); + assertThat(cmdContext.open().tip().getAddedFiles().size()).isEqualTo(1); + } + + @Test + public void shouldOverwriteExistingFiles() throws IOException { + File testFile = temporaryFolder.newFile(); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", testFile, true)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + String changeSet2 = hgModifyCommand.execute(request2); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet2); + assertThat(cmdContext.open().tip().getModifiedFiles().size()).isEqualTo(1); + assertThat(cmdContext.open().tip().getModifiedFiles().get(0)).isEqualTo("a.txt"); + } + + @Test(expected = AlreadyExistsException.class) + public void shouldThrowFileAlreadyExistsException() throws IOException { + + File testFile = temporaryFolder.newFile(); + new FileOutputStream(testFile).write(21); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request2); + } + + @Test + public void shouldModifyExistingFile() throws IOException { + File testFile = temporaryFolder.newFile("a.txt"); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.ModifyFileRequest("a.txt", testFile)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + String changeSet2 = hgModifyCommand.execute(request2); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet2); + assertThat(cmdContext.open().tip().getModifiedFiles().size()).isEqualTo(1); + assertThat(cmdContext.open().tip().getModifiedFiles().get(0)).isEqualTo(testFile.getName()); + } + + @Test(expected = NotFoundException.class) + public void shouldThrowNotFoundExceptionIfFileDoesNotExist() throws IOException { + File testFile = temporaryFolder.newFile("Answer.txt"); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.ModifyFileRequest("Answer.txt", testFile)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request2); + } + + @Test(expected = NullPointerException.class) + public void shouldThrowNPEIfAuthorIsMissing() throws IOException { + File testFile = temporaryFolder.newFile(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + hgModifyCommand.execute(request); + } + + @Test(expected = NullPointerException.class) + public void shouldThrowNPEIfCommitMessageIsMissing() throws IOException { + File testFile = temporaryFolder.newFile(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + hgModifyCommand.execute(request); + } + + @Test(expected = NoChangesMadeException.class) + public void shouldThrowNoChangesMadeExceptionIfEmptyLocalChangesetAfterRequest() { + hgModifyCommand.execute(new ModifyCommandRequest()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java index b9e720fc71..202b2d20f1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java @@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; import com.google.inject.util.Providers; +import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.jboss.resteasy.core.Dispatcher; @@ -24,6 +25,7 @@ import sonia.scm.repository.api.MergeDryRunCommandResult; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.MergeCommand; +import sonia.scm.user.User; import sonia.scm.web.VndMediaType; import java.net.URL; @@ -32,6 +34,7 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static sonia.scm.repository.RepositoryTestData.createHeartOfGold; @@ -105,6 +108,7 @@ public class MergeResourceTest extends RepositoryTestBase { @Test void shouldHandleSuccessfulMerge() throws Exception { when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.success()); + mockUser(); URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json"); byte[] mergeCommandJson = Resources.toByteArray(url); @@ -122,6 +126,7 @@ public class MergeResourceTest extends RepositoryTestBase { @Test void shouldHandleFailedMerge() throws Exception { when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.failure(asList("file1", "file2"))); + mockUser(); URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json"); byte[] mergeCommandJson = Resources.toByteArray(url); @@ -189,5 +194,12 @@ public class MergeResourceTest extends RepositoryTestBase { assertThat(response.getStatus()).isEqualTo(204); } + + + private void mockUser() { + PrincipalCollection collection = mock(PrincipalCollection.class); + when(subject.getPrincipals()).thenReturn(collection); + when(collection.oneByType(User.class)).thenReturn(new User("dummy")); + } } }