diff --git a/scm-core/pom.xml b/scm-core/pom.xml index f19d50064d..8437b256b2 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -131,6 +131,12 @@ mapstruct-jdk8 + + org.mapstruct + mapstruct-processor + provided + + com.webcohesion.enunciate @@ -208,6 +214,12 @@ shiro-unit test + + org.hibernate + hibernate-validator + 5.3.6.Final + compile + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java similarity index 100% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/MergeCommandDto.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java index 4ec78c9bdc..33f4a482ac 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java @@ -68,6 +68,10 @@ public final class BranchCommandBuilder { return command.branch(request); } + public void delete(String branchName) { + command.deleteOrClose(branchName); + } + private BranchCommand command; private BranchRequest request = new BranchRequest(); } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java index 917b81391f..2836fc537d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java @@ -178,7 +178,7 @@ public final class LogCommandBuilder logger.debug("get changeset for {} with disabled cache", id); } - changeset = logCommand.getChangeset(id); + changeset = logCommand.getChangeset(id, request); } else { @@ -192,7 +192,7 @@ public final class LogCommandBuilder logger.debug("get changeset for {}", id); } - changeset = logCommand.getChangeset(id); + changeset = logCommand.getChangeset(id, request); if (changeset != null) { 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 0a2267e888..6c4324c259 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 @@ -6,6 +6,8 @@ import sonia.scm.repository.spi.MergeCommand; import sonia.scm.repository.spi.MergeCommandRequest; import sonia.scm.repository.util.AuthorUtil; +import java.util.Set; + /** * Use this {@link MergeCommandBuilder} to merge two branches of a repository ({@link #executeMerge()}) or to check if * the branches could be merged without conflicts ({@link #dryRun()}). To do so, you have to specify the name of @@ -55,6 +57,24 @@ public class MergeCommandBuilder { this.mergeCommand = mergeCommand; } + /** + * Use this to check if merge-strategy is supported by mergeCommand. + * + * @return boolean. + */ + public boolean isSupported(MergeStrategy strategy) { + return mergeCommand.isSupported(strategy); + } + + /** + * Use this to get a Set of all supported merge strategies by merge command. + * + * @return boolean. + */ + public Set getSupportedMergeStrategies() { + return mergeCommand.getSupportedMergeStrategies(); + } + /** * Use this to set the branch that should be merged into the target branch. * @@ -92,6 +112,21 @@ public class MergeCommandBuilder { return this; } + /** + * Use this to set the strategy of the merge commit manually. + * + * This is optional and for {@link #executeMerge()} only. + * + * @return This builder instance. + */ + public MergeCommandBuilder setMergeStrategy(MergeStrategy strategy) { + if (!mergeCommand.isSupported(strategy)) { + throw new IllegalArgumentException("merge strategy not supported: " + strategy); + } + request.setMergeStrategy(strategy); + return this; + } + /** * Use this to set a template for the commit message. If no message is set, a default message will be used. * diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java new file mode 100644 index 0000000000..4ccf1f706b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategy.java @@ -0,0 +1,7 @@ +package sonia.scm.repository.api; + +public enum MergeStrategy { + MERGE_COMMIT, + FAST_FORWARD_IF_POSSIBLE, + SQUASH +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java new file mode 100644 index 0000000000..ae9fbb08b4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeStrategyNotSupportedException.java @@ -0,0 +1,27 @@ +package sonia.scm.repository.api; + +import sonia.scm.BadRequestException; +import sonia.scm.repository.Repository; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class MergeStrategyNotSupportedException extends BadRequestException { + + private static final long serialVersionUID = 256498734456613496L; + + private static final String CODE = "6eRhF9gU41"; + + public MergeStrategyNotSupportedException(Repository repository, MergeStrategy strategy) { + super(entity(repository).build(), createMessage(strategy)); + } + + @Override + public String getCode() { + return CODE; + } + + private static String createMessage(MergeStrategy strategy) { + return "merge strategy " + strategy + " is not supported by this repository"; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java index c659c7fac0..d5ba7f8dca 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java @@ -41,4 +41,6 @@ import sonia.scm.repository.api.BranchRequest; */ public interface BranchCommand { Branch branch(BranchRequest name); + + void deleteOrClose(String branchName); } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/CannotDeleteDefaultBranchException.java b/scm-core/src/main/java/sonia/scm/repository/spi/CannotDeleteDefaultBranchException.java new file mode 100644 index 0000000000..0053d5384a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/CannotDeleteDefaultBranchException.java @@ -0,0 +1,19 @@ +package sonia.scm.repository.spi; + +import sonia.scm.ContextEntry; +import sonia.scm.ExceptionWithContext; +import sonia.scm.repository.Repository; + +public class CannotDeleteDefaultBranchException extends ExceptionWithContext { + + public static final String CODE = "78RhWxTIw1"; + + public CannotDeleteDefaultBranchException(Repository repository, String branchName) { + super(ContextEntry.ContextBuilder.entity("Branch", branchName).in(repository).build(), "default branch cannot be deleted"); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java index f4babcee72..21d9ece4de 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommand.java @@ -49,7 +49,7 @@ import java.io.IOException; */ public interface LogCommand { - Changeset getChangeset(String id) throws IOException; + Changeset getChangeset(String id, LogCommandRequest request) throws IOException; ChangesetPagingResult getChangesets(LogCommandRequest request) throws IOException; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java index 0a3680f6b3..a62e373dca 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java @@ -2,9 +2,16 @@ package sonia.scm.repository.spi; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; +import sonia.scm.repository.api.MergeStrategy; + +import java.util.Set; public interface MergeCommand { MergeCommandResult merge(MergeCommandRequest request); MergeDryRunCommandResult dryRun(MergeCommandRequest request); + + boolean isSupported(MergeStrategy strategy); + + Set getSupportedMergeStrategies(); } 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 223cf8c49e..0751920d01 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,6 +5,7 @@ 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.api.MergeStrategy; import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor; import java.io.Serializable; @@ -17,6 +18,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl private String targetBranch; private Person author; private String messageTemplate; + private MergeStrategy mergeStrategy; public String getBranchToMerge() { return branchToMerge; @@ -50,6 +52,14 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl this.messageTemplate = messageTemplate; } + public MergeStrategy getMergeStrategy() { + return mergeStrategy; + } + + public void setMergeStrategy(MergeStrategy mergeStrategy) { + this.mergeStrategy = mergeStrategy; + } + public boolean isValid() { return !Strings.isNullOrEmpty(getBranchToMerge()) && !Strings.isNullOrEmpty(getTargetBranch()); @@ -74,12 +84,13 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl return Objects.equal(branchToMerge, other.branchToMerge) && Objects.equal(targetBranch, other.targetBranch) - && Objects.equal(author, other.author); + && Objects.equal(author, other.author) + && Objects.equal(mergeStrategy, other.mergeStrategy); } @Override public int hashCode() { - return Objects.hashCode(branchToMerge, targetBranch, author); + return Objects.hashCode(branchToMerge, targetBranch, author, mergeStrategy); } @Override @@ -88,6 +99,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl .add("branchToMerge", branchToMerge) .add("targetBranch", targetBranch) .add("author", author) + .add("mergeStrategy", mergeStrategy) .toString(); } } 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 a1073e24f2..eaa9487f3c 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 @@ -8,7 +8,7 @@ import sonia.scm.repository.Repository; import java.io.File; import java.io.IOException; -public abstract class SimpleWorkdirFactory implements WorkdirFactory { +public abstract class SimpleWorkdirFactory implements WorkdirFactory { private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class); @@ -19,11 +19,11 @@ public abstract class SimpleWorkdirFactory implements WorkdirFactory } @Override - public WorkingCopy createWorkingCopy(C context, String initialBranch) { + public WorkingCopy createWorkingCopy(C context, String initialBranch) { try { File directory = workdirProvider.createNewWorkdir(); - ParentAndClone parentAndClone = cloneRepository(context, directory, initialBranch); - return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::close, directory); + ParentAndClone parentAndClone = cloneRepository(context, directory, initialBranch); + return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::closeWorkdir, this::closeCentral, directory); } catch (IOException e) { throw new InternalRepositoryException(getScmRepository(context), "could not clone repository in temporary directory", e); } @@ -32,12 +32,15 @@ public abstract class SimpleWorkdirFactory implements WorkdirFactory protected abstract Repository getScmRepository(C context); @SuppressWarnings("squid:S00112") - // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in close + // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in closeCentral protected abstract void closeRepository(R repository) throws Exception; + @SuppressWarnings("squid:S00112") + // We do allow implementations to throw arbitrary exceptions here, so that we can handle them in closeWorkdir + protected abstract void closeWorkdirInternal(W workdir) throws Exception; - protected abstract ParentAndClone cloneRepository(C context, File target, String initialBranch) throws IOException; + protected abstract ParentAndClone cloneRepository(C context, File target, String initialBranch) throws IOException; - private void close(R repository) { + private void closeCentral(R repository) { try { closeRepository(repository); } catch (Exception e) { @@ -45,11 +48,19 @@ public abstract class SimpleWorkdirFactory implements WorkdirFactory } } - protected static class ParentAndClone { - private final R parent; - private final R clone; + private void closeWorkdir(W repository) { + try { + closeWorkdirInternal(repository); + } catch (Exception e) { + logger.warn("could not close temporary repository clone", e); + } + } - public ParentAndClone(R parent, R clone) { + protected static class ParentAndClone { + private final R parent; + private final W clone; + + public ParentAndClone(R parent, W clone) { this.parent = parent; this.clone = clone; } @@ -58,7 +69,7 @@ public abstract class SimpleWorkdirFactory implements WorkdirFactory return parent; } - public R getClone() { + public W getClone() { return clone; } } diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java index bddf03adaa..e1df5e99f3 100644 --- a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java @@ -1,5 +1,5 @@ package sonia.scm.repository.util; -public interface WorkdirFactory { - WorkingCopy createWorkingCopy(C context, String initialBranch); +public interface WorkdirFactory { + WorkingCopy createWorkingCopy(C context, String initialBranch); } diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java index 3c96184142..12c859fb8c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java +++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java @@ -8,23 +8,25 @@ import java.io.File; import java.io.IOException; import java.util.function.Consumer; -public class WorkingCopy implements AutoCloseable { +public class WorkingCopy implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(WorkingCopy.class); private final File directory; - private final R workingRepository; + private final W workingRepository; private final R centralRepository; - private final Consumer cleanup; + private final Consumer cleanupWorkdir; + private final Consumer cleanupCentral; - public WorkingCopy(R workingRepository, R centralRepository, Consumer cleanup, File directory) { + public WorkingCopy(W workingRepository, R centralRepository, Consumer cleanupWorkdir, Consumer cleanupCentral, File directory) { this.directory = directory; this.workingRepository = workingRepository; this.centralRepository = centralRepository; - this.cleanup = cleanup; + this.cleanupCentral = cleanupCentral; + this.cleanupWorkdir = cleanupWorkdir; } - public R getWorkingRepository() { + public W getWorkingRepository() { return workingRepository; } @@ -39,8 +41,8 @@ public class WorkingCopy implements AutoCloseable { @Override public void close() { try { - cleanup.accept(workingRepository); - cleanup.accept(centralRepository); + cleanupWorkdir.accept(workingRepository); + cleanupCentral.accept(centralRepository); IOUtil.delete(directory); } catch (IOException e) { LOG.warn("could not delete temporary workdir '{}'", directory, 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 4a1a1a4179..04e7b72202 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 @@ -24,14 +24,14 @@ public class SimpleWorkdirFactoryTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - private SimpleWorkdirFactory simpleWorkdirFactory; + private SimpleWorkdirFactory simpleWorkdirFactory; private String initialBranchForLastCloneCall; @Before public void initFactory() throws IOException { WorkdirProvider workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); - simpleWorkdirFactory = new SimpleWorkdirFactory(workdirProvider) { + simpleWorkdirFactory = new SimpleWorkdirFactory(workdirProvider) { @Override protected Repository getScmRepository(Context context) { return REPOSITORY; @@ -43,7 +43,12 @@ public class SimpleWorkdirFactoryTest { } @Override - protected ParentAndClone cloneRepository(Context context, File target, String initialBranch) { + protected void closeWorkdirInternal(Closeable workdir) throws Exception { + workdir.close(); + } + + @Override + protected ParentAndClone cloneRepository(Context context, File target, String initialBranch) { initialBranchForLastCloneCall = initialBranch; return new ParentAndClone<>(parent, clone); } @@ -53,7 +58,7 @@ public class SimpleWorkdirFactoryTest { @Test public void shouldCreateParentAndClone() { Context context = new Context(); - try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) { + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) { assertThat(workingCopy.getCentralRepository()).isSameAs(parent); assertThat(workingCopy.getWorkingRepository()).isSameAs(clone); } @@ -62,7 +67,7 @@ public class SimpleWorkdirFactoryTest { @Test public void shouldCloseParent() throws IOException { Context context = new Context(); - try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {} + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {} verify(parent).close(); } @@ -70,7 +75,7 @@ public class SimpleWorkdirFactoryTest { @Test public void shouldCloseClone() throws IOException { Context context = new Context(); - try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {} + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {} verify(clone).close(); } @@ -78,7 +83,7 @@ public class SimpleWorkdirFactoryTest { @Test public void shouldPropagateInitialBranch() { Context context = new Context(); - try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, "some")) { + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context, "some")) { assertThat(initialBranchForLastCloneCall).isEqualTo("some"); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java index d3ed353677..9b2be467e8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java @@ -4,5 +4,5 @@ import org.eclipse.jgit.lib.Repository; import sonia.scm.repository.spi.GitContext; import sonia.scm.repository.util.WorkdirFactory; -public interface GitWorkdirFactory extends WorkdirFactory { +public interface GitWorkdirFactory extends WorkdirFactory { } 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 2531888a8b..adf7878221 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 @@ -142,7 +142,7 @@ class AbstractGitCommand } > R inClone(Function workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, initialBranch)) { + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, initialBranch)) { Repository repository = workingCopy.getWorkingRepository(); logger.debug("cloned repository to folder {}", repository.getWorkTree()); return workerSupplier.apply(new Git(repository)).run(); @@ -152,19 +152,28 @@ class AbstractGitCommand } ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException { + sonia.scm.repository.Repository scmRepository = context.getRepository(); + return resolveRevisionOrThrowNotFound(repository, revision, scmRepository); + } + + static ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision, sonia.scm.repository.Repository scmRepository) throws IOException { ObjectId resolved = repository.resolve(revision); if (resolved == null) { - throw notFound(entity("Revision", revision).in(context.getRepository())); + throw notFound(entity("Revision", revision).in(scmRepository)); } else { return resolved; } } - abstract class GitCloneWorker { + abstract static class GitCloneWorker { private final Git clone; + private final GitContext context; + private final sonia.scm.repository.Repository repository; - GitCloneWorker(Git clone) { + GitCloneWorker(Git clone, GitContext context, sonia.scm.repository.Repository repository) { this.clone = clone; + this.context = context; + this.repository = repository; } abstract R run() throws IOException; @@ -173,6 +182,10 @@ class AbstractGitCommand return clone; } + GitContext getContext() { + return context; + } + void checkOutBranch(String branchName) throws IOException { try { clone.checkout().setName(branchName).call(); @@ -199,7 +212,7 @@ class AbstractGitCommand ObjectId resolveRevision(String revision) throws IOException { ObjectId resolved = clone.getRepository().resolve(revision); if (resolved == null) { - return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision); + return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision, context.getRepository()); } else { return resolved; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java index e8675068b9..fd3c25cf39 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java @@ -33,51 +33,120 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.CannotDeleteCurrentBranchException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.transport.PushResult; -import org.eclipse.jgit.transport.RemoteRefUpdate; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Branch; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.PreReceiveRepositoryHookEvent; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.api.BranchRequest; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.api.HookBranchProvider; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.repository.api.HookFeature; -import java.util.stream.StreamSupport; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static sonia.scm.ContextEntry.ContextBuilder.entity; public class GitBranchCommand extends AbstractGitCommand implements BranchCommand { - private final GitWorkdirFactory workdirFactory; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; - GitBranchCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) { + GitBranchCommand(GitContext context, Repository repository, HookContextFactory hookContextFactory, ScmEventBus eventBus) { super(context, repository); - this.workdirFactory = workdirFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; } @Override public Branch branch(BranchRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, request.getParentBranch())) { - Git clone = new Git(workingCopy.getWorkingRepository()); - Ref ref = clone.branchCreate().setName(request.getNewBranch()).call(); - Iterable call = clone.push().add(request.getNewBranch()).call(); - StreamSupport.stream(call.spliterator(), false) - .flatMap(pushResult -> pushResult.getRemoteUpdates().stream()) - .filter(remoteRefUpdate -> remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK) - .findFirst() - .ifPresent(r -> this.handlePushError(r, request, context.getRepository())); + try (Git git = new Git(context.open())) { + RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.createHookEvent(request.getNewBranch())); + eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent)); + Ref ref = git.branchCreate().setStartPoint(request.getParentBranch()).setName(request.getNewBranch()).call(); + eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); return Branch.normalBranch(request.getNewBranch(), GitUtil.getId(ref.getObjectId())); - } catch (GitAPIException ex) { + } catch (GitAPIException | IOException ex) { throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex); } } - private void handlePushError(RemoteRefUpdate remoteRefUpdate, BranchRequest request, Repository repository) { - if (remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK) { - // TODO handle failed remote update - throw new IntegrateChangesFromWorkdirException(repository, - String.format("Could not push new branch '%s' into central repository", request.getNewBranch())); + @Override + public void deleteOrClose(String branchName) { + try (Git gitRepo = new Git(context.open())) { + RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.deleteHookEvent(branchName)); + eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent)); + gitRepo + .branchDelete() + .setBranchNames(branchName) + .setForce(true) + .call(); + eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); + } catch (CannotDeleteCurrentBranchException e) { + throw new CannotDeleteDefaultBranchException(context.getRepository(), branchName); + } catch (GitAPIException | IOException ex) { + throw new InternalRepositoryException(entity(context.getRepository()), String.format("Could not delete branch: %s", branchName)); + } + } + + private RepositoryHookEvent createBranchHookEvent(BranchHookContextProvider hookEvent) { + HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository()); + return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE); + } + + private static class BranchHookContextProvider extends HookContextProvider { + private final List newBranches; + private final List deletedBranches; + + private BranchHookContextProvider(List newBranches, List deletedBranches) { + this.newBranches = newBranches; + this.deletedBranches = deletedBranches; + } + + static BranchHookContextProvider createHookEvent(String newBranch) { + return new BranchHookContextProvider(singletonList(newBranch), emptyList()); + } + + static BranchHookContextProvider deleteHookEvent(String deletedBranch) { + return new BranchHookContextProvider(emptyList(), singletonList(deletedBranch)); + } + + @Override + public Set getSupportedFeatures() { + return singleton(HookFeature.BRANCH_PROVIDER); + } + + @Override + public HookBranchProvider getBranchProvider() { + return new HookBranchProvider() { + @Override + public List getCreatedOrModified() { + return newBranches; + } + + @Override + public List getDeletedOrClosed() { + return deletedBranches; + } + }; + } + + @Override + public HookChangesetProvider getChangesetProvider() { + return r -> new HookChangesetResponse(emptyList()); } } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java new file mode 100644 index 0000000000..64a20a33cb --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java @@ -0,0 +1,36 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand; +import org.eclipse.jgit.api.MergeResult; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.MergeCommandResult; + +import java.io.IOException; + +class GitFastForwardIfPossible extends GitMergeStrategy { + + private GitMergeStrategy fallbackMerge; + + GitFastForwardIfPossible(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { + super(clone, request, context, repository); + fallbackMerge = new GitMergeCommit(clone, request, context, repository); + } + + @Override + MergeCommandResult run() throws IOException { + MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); + if (fastForwardResult.getMergeStatus().isSuccessful()) { + push(); + return MergeCommandResult.success(); + } else { + return fallbackMerge.run(); + } + } + + private MergeResult mergeWithFastForwardOnlyMode() throws IOException { + MergeCommand mergeCommand = getClone().merge(); + mergeCommand.setFastForward(MergeCommand.FastForwardMode.FF_ONLY); + return doMergeInClone(mergeCommand); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java index a42abca5f0..8c44f33f7f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java @@ -106,7 +106,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand * @return */ @Override - public Changeset getChangeset(String revision) + public Changeset getChangeset(String revision, LogCommandRequest request) { if (logger.isDebugEnabled()) { @@ -131,7 +131,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand if (commit != null) { converter = new GitChangesetConverter(gr, revWalk); - changeset = converter.createChangeset(commit); + + if (isBranchRequested(request)) { + String branch = request.getBranch(); + if (isMergedIntoBranch(gr, revWalk, commit, branch)) { + logger.trace("returning commit {} with branch {}", commit.getId(), branch); + changeset = converter.createChangeset(commit, branch); + } else { + logger.debug("returning null, because commit {} was not merged into branch {}", commit.getId(), branch); + } + } else { + changeset = converter.createChangeset(commit); + } } else if (logger.isWarnEnabled()) { @@ -157,6 +168,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand return changeset; } + private boolean isMergedIntoBranch(Repository repository, RevWalk revWalk, RevCommit commit, String branchName) throws IOException { + return revWalk.isMergedInto(commit, findHeadCommitOfBranch(repository, revWalk, branchName)); + } + + private boolean isBranchRequested(LogCommandRequest request) { + return request != null && !Strings.isNullOrEmpty(request.getBranch()); + } + + private RevCommit findHeadCommitOfBranch(Repository repository, RevWalk revWalk, String branchName) throws IOException { + return revWalk.parseCommit(GitUtil.getCommit(repository, revWalk, repository.findRef(branchName))); + } + /** * Method 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 5643c858b5..226dd0f285 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,36 +1,30 @@ package sonia.scm.repository.spi; -import com.google.common.base.Strings; -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.lib.ObjectId; +import com.google.common.collect.ImmutableSet; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.ResolveMerger; -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.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; +import sonia.scm.repository.api.MergeStrategy; +import sonia.scm.repository.api.MergeStrategyNotSupportedException; import java.io.IOException; -import java.text.MessageFormat; +import java.util.Set; + +import static org.eclipse.jgit.merge.MergeStrategy.RECURSIVE; public class GitMergeCommand extends AbstractGitCommand implements MergeCommand { - private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class); - - private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n", - "Merge of branch {0} into {1}", - "", - "Automatic merge by SCM-Manager."); - private final GitWorkdirFactory workdirFactory; + private static final Set STRATEGIES = ImmutableSet.of( + MergeStrategy.MERGE_COMMIT, + MergeStrategy.FAST_FORWARD_IF_POSSIBLE, + MergeStrategy.SQUASH + ); + GitMergeCommand(GitContext context, sonia.scm.repository.Repository repository, GitWorkdirFactory workdirFactory) { super(context, repository); this.workdirFactory = workdirFactory; @@ -38,14 +32,30 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand @Override public MergeCommandResult merge(MergeCommandRequest request) { - return inClone(clone -> new MergeWorker(clone, request), workdirFactory, request.getTargetBranch()); + return mergeWithStrategy(request); + } + + private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) { + switch(request.getMergeStrategy()) { + case SQUASH: + return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + + case FAST_FORWARD_IF_POSSIBLE: + return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + + case MERGE_COMMIT: + return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workdirFactory, request.getTargetBranch()); + + default: + throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy()); + } } @Override public MergeDryRunCommandResult dryRun(MergeCommandRequest request) { try { Repository repository = context.open(); - ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); + ResolveMerger merger = (ResolveMerger) RECURSIVE.newMerger(repository, true); return new MergeDryRunCommandResult( merger.merge( resolveRevisionOrThrowNotFound(repository, request.getBranchToMerge()), @@ -55,64 +65,14 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand } } - private class MergeWorker extends GitCloneWorker { - - private final String target; - private final String toMerge; - private final Person author; - private final String messageTemplate; - - private MergeWorker(Git clone, MergeCommandRequest request) { - super(clone); - this.target = request.getTargetBranch(); - this.toMerge = request.getBranchToMerge(); - this.author = request.getAuthor(); - this.messageTemplate = request.getMessageTemplate(); - } - - @Override - MergeCommandResult run() throws IOException { - MergeResult result = doMergeInClone(); - if (result.getMergeStatus().isSuccessful()) { - doCommit(); - push(); - return MergeCommandResult.success(); - } else { - return analyseFailure(result); - } - } - - private MergeResult doMergeInClone() throws IOException { - MergeResult result; - try { - ObjectId sourceRevision = resolveRevision(toMerge); - result = getClone().merge() - .setFastForward(FastForwardMode.NO_FF) - .setCommit(false) // we want to set the author manually - .include(toMerge, sourceRevision) - .call(); - } catch (GitAPIException e) { - throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + toMerge + " into " + target, e); - } - return result; - } - - private void doCommit() { - logger.debug("merged branch {} into {}", toMerge, target); - doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author); - } - - private String determineMessageTemplate() { - if (Strings.isNullOrEmpty(messageTemplate)) { - return MERGE_COMMIT_MESSAGE_TEMPLATE; - } else { - return messageTemplate; - } - } - - 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()); - } + @Override + public boolean isSupported(MergeStrategy strategy) { + return STRATEGIES.contains(strategy); } + + @Override + public Set getSupportedMergeStrategies() { + return STRATEGIES; + } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java new file mode 100644 index 0000000000..6aa68a0ea8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -0,0 +1,31 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand; +import org.eclipse.jgit.api.MergeResult; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.MergeCommandResult; + +import java.io.IOException; + +class GitMergeCommit extends GitMergeStrategy { + + GitMergeCommit(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { + super(clone, request, context, repository); + } + + @Override + MergeCommandResult run() throws IOException { + MergeCommand mergeCommand = getClone().merge(); + mergeCommand.setFastForward(MergeCommand.FastForwardMode.NO_FF); + MergeResult result = doMergeInClone(mergeCommand); + + if (result.getMergeStatus().isSuccessful()) { + doCommit(); + push(); + return MergeCommandResult.success(); + } else { + return analyseFailure(result); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java new file mode 100644 index 0000000000..1d53b99c99 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -0,0 +1,72 @@ +package sonia.scm.repository.spi; + +import com.google.common.base.Strings; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand; +import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Person; +import sonia.scm.repository.api.MergeCommandResult; + +import java.io.IOException; +import java.text.MessageFormat; + +abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker { + + private static final Logger logger = LoggerFactory.getLogger(GitMergeStrategy.class); + + private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n", + "Merge of branch {0} into {1}", + "", + "Automatic merge by SCM-Manager."); + + private final String target; + private final String toMerge; + private final Person author; + private final String messageTemplate; + + GitMergeStrategy(Git clone, MergeCommandRequest request, GitContext context, sonia.scm.repository.Repository repository) { + super(clone, context, repository); + this.target = request.getTargetBranch(); + this.toMerge = request.getBranchToMerge(); + this.author = request.getAuthor(); + this.messageTemplate = request.getMessageTemplate(); + } + + MergeResult doMergeInClone(MergeCommand mergeCommand) throws IOException { + MergeResult result; + try { + ObjectId sourceRevision = resolveRevision(toMerge); + mergeCommand + .setCommit(false) // we want to set the author manually + .include(toMerge, sourceRevision); + + result = mergeCommand.call(); + } catch (GitAPIException e) { + throw new InternalRepositoryException(getContext().getRepository(), "could not merge branch " + toMerge + " into " + target, e); + } + return result; + } + + void doCommit() { + logger.debug("merged branch {} into {}", toMerge, target); + doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author); + } + + private String determineMessageTemplate() { + if (Strings.isNullOrEmpty(messageTemplate)) { + return MERGE_COMMIT_MESSAGE_TEMPLATE; + } else { + return messageTemplate; + } + } + + MergeCommandResult analyseFailure(MergeResult result) { + logger.info("could not merge branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet()); + return MergeCommandResult.failure(result.getConflicts().keySet()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java new file mode 100644 index 0000000000..b688956404 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java @@ -0,0 +1,31 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.MergeCommandResult; +import org.eclipse.jgit.api.MergeCommand; + +import java.io.IOException; + +class GitMergeWithSquash extends GitMergeStrategy { + + GitMergeWithSquash(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { + super(clone, request, context, repository); + } + + @Override + MergeCommandResult run() throws IOException { + MergeCommand mergeCommand = getClone().merge(); + mergeCommand.setSquash(true); + MergeResult result = doMergeInClone(mergeCommand); + + if (result.getMergeStatus().isSuccessful()) { + doCommit(); + push(); + return MergeCommandResult.success(); + } else { + return analyseFailure(result); + } + } +} 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 a68be0a4da..19234c32e3 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 @@ -46,7 +46,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman private final ModifyCommandRequest request; ModifyWorker(Git clone, ModifyCommandRequest request) { - super(clone); + super(clone, context, repository); this.workDir = clone.getRepository().getWorkTree(); this.request = request; } 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 dc43b8d9b7..e1ae58ada5 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 @@ -35,10 +35,12 @@ package sonia.scm.repository.spi; import com.google.common.collect.ImmutableSet; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Feature; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.IOException; @@ -64,6 +66,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider Command.DIFF_RESULT, Command.LOG, Command.TAGS, + Command.BRANCH, Command.BRANCHES, Command.INCOMING, Command.OUTGOING, @@ -77,10 +80,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) { + public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { this.handler = handler; this.repository = repository; this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); } @@ -133,7 +138,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public BranchCommand getBranchCommand() { - return new GitBranchCommand(context, repository, handler.getWorkdirFactory()); + return new GitBranchCommand(context, repository, hookContextFactory, eventBus); } /** @@ -292,4 +297,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider private final Repository repository; private final LfsBlobStoreFactory lfsBlobStoreFactory; + + private final HookContextFactory hookContextFactory; + + private final ScmEventBus eventBus; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 547c6b25f8..7fc5fb27c4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -36,9 +36,11 @@ package sonia.scm.repository.spi; import com.google.inject.Inject; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.event.ScmEventBus; import sonia.scm.plugin.Extension; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory; /** @@ -51,12 +53,16 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { private final GitRepositoryHandler handler; private final GitRepositoryConfigStoreProvider storeProvider; private final LfsBlobStoreFactory lfsBlobStoreFactory; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) { + public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { this.handler = handler; this.storeProvider = storeProvider; this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; } @Override @@ -64,7 +70,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { GitRepositoryServiceProvider provider = null; if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory); + provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus); } return provider; 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 a0fda7cd3b..f7b0c5567c 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 @@ -18,7 +18,7 @@ import java.io.IOException; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; -public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory implements GitWorkdirFactory { +public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory implements GitWorkdirFactory { @Inject public SimpleGitWorkdirFactory(WorkdirProvider workdirProvider) { @@ -26,7 +26,7 @@ public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory cloneRepository(GitContext context, File target, String initialBranch) { + public ParentAndClone cloneRepository(GitContext context, File target, String initialBranch) { try { Repository clone = Git.cloneRepository() .setURI(createScmTransportProtocolUri(context.getDirectory())) @@ -60,6 +60,13 @@ public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory b.getName().equals("new_branch")).isEmpty(); + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isEmpty(); BranchRequest branchRequest = new BranchRequest(); branchRequest.setNewBranch("new_branch"); - new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest); + createCommand().branch(branchRequest); - Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); + } + + @Test + public void shouldDeleteBranch() throws IOException { + GitContext context = createContext(); + String branchToBeDeleted = "squash"; + createCommand().deleteOrClose(branchToBeDeleted); + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals(branchToBeDeleted)).isEmpty(); + } + + @Test + public void shouldThrowExceptionWhenDeletingDefaultBranch() { + String branchToBeDeleted = "master"; + assertThrows(CannotDeleteDefaultBranchException.class, () -> createCommand().deleteOrClose(branchToBeDeleted)); + } + + private GitBranchCommand createCommand() { + return new GitBranchCommand(createContext(), repository, hookContextFactory, eventBus); } private List readBranches(GitContext context) throws IOException { return new GitBranchesCommand(context, repository).getBranches(); } + + @Test + public void shouldPostCreateEvents() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + doNothing().when(eventBus).post(captor.capture()); + when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext); + + BranchRequest branchRequest = new BranchRequest(); + branchRequest.setParentBranch("mergeable"); + branchRequest.setNewBranch("new_branch"); + + createCommand().branch(branchRequest); + + List events = captor.getAllValues(); + assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class); + assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class); + + PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0); + assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).containsExactly("new_branch"); + assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).isEmpty(); + } + + @Test + public void shouldPostDeleteEvents() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + doNothing().when(eventBus).post(captor.capture()); + when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext); + + createCommand().deleteOrClose("squash"); + + List events = captor.getAllValues(); + assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class); + assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class); + + PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0); + assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).containsExactly("squash"); + assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).isEmpty(); + } + + private HookContext createMockedContext(InvocationOnMock invocation) { + HookContext mock = mock(HookContext.class); + when(mock.getBranchProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getBranchProvider()); + return mock; + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index 06e9b17fe7..71dac42f72 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -35,7 +35,11 @@ package sonia.scm.repository.spi; import com.google.common.io.Files; +import org.assertj.core.api.Assertions; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitRepositoryConfig; @@ -51,14 +55,18 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; /** * Unit tests for {@link GitLogCommand}. * * @author Sebastian Sdorra */ +@RunWith(MockitoJUnitRunner.class) public class GitLogCommandTest extends AbstractGitCommandTestBase { + @Mock + LogCommandRequest request; /** * Tests log command with the usage of a default branch. @@ -171,7 +179,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase public void testGetCommit() { GitLogCommand command = createCommand(); - Changeset c = command.getChangeset("435df2f061add3589cb3"); + Changeset c = command.getChangeset("435df2f061add3589cb3", null); assertNotNull(c); String revision = "435df2f061add3589cb326cc64be9b9c3897ceca"; @@ -193,6 +201,23 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertThat(modifications.getAdded(), contains("a.txt", "b.txt")); } + @Test + public void commitShouldContainBranchIfLogCommandRequestHasBranch() + { + when(request.getBranch()).thenReturn("master"); + GitLogCommand command = createCommand(); + Changeset c = command.getChangeset("435df2f061add3589cb3", request); + + Assertions.assertThat(c.getBranches()).containsOnly("master"); + } + + @Test + public void shouldNotReturnCommitFromDifferentBranch() { + when(request.getBranch()).thenReturn("master"); + Changeset changeset = createCommand().getChangeset("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", request); + Assertions.assertThat(changeset).isNull(); + } + @Test public void testGetRange() { 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 5586a2f710..fcd721c3a2 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 @@ -15,10 +15,12 @@ import org.junit.Test; import sonia.scm.NotFoundException; import sonia.scm.repository.Person; import sonia.scm.repository.api.MergeCommandResult; +import sonia.scm.repository.api.MergeStrategy; import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.user.User; import java.io.IOException; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -62,6 +64,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { MergeCommandRequest request = new MergeCommandRequest(); request.setTargetBranch("master"); request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); MergeCommandResult mergeCommandResult = command.merge(request); @@ -88,6 +91,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { MergeCommandRequest request = new MergeCommandRequest(); request.setTargetBranch("master"); request.setBranchToMerge("empty_merge"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); MergeCommandResult mergeCommandResult = command.merge(request); @@ -109,6 +113,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { request.setTargetBranch("master"); request.setBranchToMerge("mergeable"); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); MergeCommandResult mergeCommandResult = command.merge(request); @@ -132,6 +137,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { MergeCommandRequest request = new MergeCommandRequest(); request.setTargetBranch("master"); request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); request.setMessageTemplate("simple"); @@ -152,6 +158,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { MergeCommandRequest request = new MergeCommandRequest(); request.setBranchToMerge("test-branch"); request.setTargetBranch("master"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); MergeCommandResult mergeCommandResult = command.merge(request); @@ -173,6 +180,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { MergeCommandRequest request = new MergeCommandRequest(); request.setTargetBranch("master"); request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); MergeCommandResult mergeCommandResult = command.merge(request); @@ -192,6 +200,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); request.setTargetBranch("mergeable"); request.setBranchToMerge("master"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); MergeCommandResult mergeCommandResult = command.merge(request); @@ -211,12 +220,112 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n"); } + @Test + public void shouldSquashCommitsIfSquashIsEnabled() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setBranchToMerge("squash"); + request.setTargetBranch("master"); + request.setMessageTemplate("this is a squash"); + request.setMergeStrategy(MergeStrategy.SQUASH); + + MergeCommandResult mergeCommandResult = command.merge(request); + + Repository repository = createContext().open(); + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(message).isEqualTo("this is a squash"); + } + + @Test + public void shouldSquashThreeCommitsIntoOne() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setBranchToMerge("squash"); + request.setTargetBranch("master"); + request.setMessageTemplate("squash three commits"); + request.setMergeStrategy(MergeStrategy.SQUASH); + Repository gitRepository = createContext().open(); + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable commits = new Git(gitRepository).log().add(gitRepository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(message).isEqualTo("squash three commits"); + + GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext(), repository); + List changes = modificationsCommand.getModifications("master").getAdded(); + assertThat(changes.size()).isEqualTo(3); + } + + + @Test + public void shouldMergeWithFastForward() throws IOException, GitAPIException { + Repository repository = createContext().open(); + + ObjectId featureBranchHead = new Git(repository).log().add(repository.resolve("squash")).setMaxCount(1).call().iterator().next().getId(); + + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setBranchToMerge("squash"); + request.setTargetBranch("master"); + request.setMergeStrategy(MergeStrategy.FAST_FORWARD_IF_POSSIBLE); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getParentCount()).isEqualTo(1); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + assertThat(mergeAuthor.getName()).isEqualTo("Philip J Fry"); + assertThat(mergeCommit.getId()).isEqualTo(featureBranchHead); + } + + @Test + public void shouldDoMergeCommitIfFastForwardIsNotPossible() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.FAST_FORWARD_IF_POSSIBLE); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); + assertThat(mergeCommit.getParentCount()).isEqualTo(2); + String message = mergeCommit.getFullMessage(); + assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); + assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det"); + assertThat(message).contains("master", "mergeable"); + } + @Test(expected = NotFoundException.class) public void shouldHandleNotExistingSourceBranchInMerge() { GitMergeCommand command = createCommand(); MergeCommandRequest request = new MergeCommandRequest(); request.setTargetBranch("mergeable"); request.setBranchToMerge("not_existing"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); command.merge(request); } @@ -225,6 +334,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { public void shouldHandleNotExistingTargetBranchInMerge() { GitMergeCommand command = createCommand(); MergeCommandRequest request = new MergeCommandRequest(); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); request.setTargetBranch("not_existing"); request.setBranchToMerge("master"); 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 cb2d434f1e..d61a48508d 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 @@ -45,7 +45,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); File masterRepo = createRepositoryDirectory(); - try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { assertThat(workingCopy.getDirectory()) .exists() @@ -62,7 +62,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { public void shouldCheckoutInitialBranch() { SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); - try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), "test-branch")) { + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), "test-branch")) { assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt")) .exists() .isFile() @@ -75,10 +75,10 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); File firstDirectory; - try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { firstDirectory = workingCopy.getDirectory(); } - try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { File secondDirectory = workingCopy.getDirectory(); assertThat(secondDirectory).isNotEqualTo(firstDirectory); } @@ -89,7 +89,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); File directory; - try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { directory = workingCopy.getWorkingRepository().getWorkTree(); } assertThat(directory).doesNotExist(); diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip index 4310e63733..8e43da1d82 100644 Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip differ diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java index 5e9e01c9d8..37e0937aca 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java @@ -36,7 +36,9 @@ import com.aragost.javahg.commands.PullCommand; import org.apache.shiro.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; import sonia.scm.repository.Branch; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.BranchRequest; import sonia.scm.repository.util.WorkingCopy; @@ -59,7 +61,7 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand { @Override public Branch branch(BranchRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(getContext(), request.getParentBranch())) { + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(getContext(), request.getParentBranch())) { com.aragost.javahg.Repository repository = workingCopy.getWorkingRepository(); Changeset emptyChangeset = createNewBranchWithEmptyCommit(request, repository); @@ -67,23 +69,46 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand { LOG.debug("Created new branch '{}' in repository {} with changeset {}", request.getNewBranch(), getRepository().getNamespaceAndName(), emptyChangeset.getNode()); - pullNewBranchIntoCentralRepository(request, workingCopy); + pullChangesIntoCentralRepository(workingCopy, request.getNewBranch()); return Branch.normalBranch(request.getNewBranch(), emptyChangeset.getNode()); } } + @Override + public void deleteOrClose(String branchName) { + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(getContext(), branchName)) { + User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class); + + LOG.debug("Closing branch '{}' in repository {}", branchName, getRepository().getNamespaceAndName()); + + com.aragost.javahg.commands.CommitCommand + .on(workingCopy.getWorkingRepository()) + .user(getFormattedUser(currentUser)) + .message(String.format("Close branch: %s", branchName)) + .closeBranch() + .execute(); + pullChangesIntoCentralRepository(workingCopy, branchName); + } catch (Exception ex) { + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity(getContext().getScmRepository()), String.format("Could not close branch: %s", branchName)); + } + } + + private String getFormattedUser(User currentUser) { + return String.format("%s <%s>", currentUser.getDisplayName(), currentUser.getMail()); + } + private Changeset createNewBranchWithEmptyCommit(BranchRequest request, com.aragost.javahg.Repository repository) { com.aragost.javahg.commands.BranchCommand.on(repository).set(request.getNewBranch()); User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class); return CommitCommand .on(repository) - .user(String.format("%s <%s>", currentUser.getDisplayName(), currentUser.getMail())) + .user(getFormattedUser(currentUser)) .message("Create new branch " + request.getNewBranch()) .execute(); } - private void pullNewBranchIntoCentralRepository(BranchRequest request, WorkingCopy workingCopy) { + private void pullChangesIntoCentralRepository(WorkingCopy workingCopy, String branch) { try { PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); workdirFactory.configure(pullCommand); @@ -91,7 +116,7 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand { } catch (Exception e) { // TODO handle failed update throw new IntegrateChangesFromWorkdirException(getRepository(), - String.format("Could not pull new branch '%s' into central repository", request.getNewBranch()), + String.format("Could not pull changes '%s' into central repository", branch), e); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java index 19a8724b69..48772ad5e5 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java @@ -35,6 +35,8 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import com.aragost.javahg.Changeset; +import com.aragost.javahg.commands.LogCommand; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import sonia.scm.repository.BrowserResult; @@ -72,10 +74,10 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { HgFileviewCommand cmd = HgFileviewCommand.on(open()); - if (!Strings.isNullOrEmpty(request.getRevision())) - { - cmd.rev(request.getRevision()); - } + String revision = MoreObjects.firstNonNull(request.getRevision(), "tip"); + Changeset c = LogCommand.on(getContext().open()).rev(revision).limit(1).single(); + + cmd.rev(c.getNode()); if (!Strings.isNullOrEmpty(request.getPath())) { @@ -98,6 +100,6 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand } FileObject file = cmd.execute(); - return new BrowserResult(MoreObjects.firstNonNull(request.getRevision(), "tip"), file); + return new BrowserResult(c.getNode(), revision, file); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java index e9de7f7471..8a9a1c8e84 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java @@ -68,7 +68,7 @@ public class HgLogCommand extends AbstractCommand implements LogCommand //~--- get methods ---------------------------------------------------------- @Override - public Changeset getChangeset(String id) { + public Changeset getChangeset(String id, LogCommandRequest request) { com.aragost.javahg.Repository repository = open(); HgLogChangesetCommand cmd = on(repository); 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 index 0294b6902b..419b7de161 100644 --- 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 @@ -29,7 +29,7 @@ public class HgModifyCommand implements ModifyCommand { @Override public String execute(ModifyCommandRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, request.getBranch())) { + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, request.getBranch())) { Repository workingRepository = workingCopy.getWorkingRepository(); request.getRequests().forEach( partialRequest -> { @@ -85,7 +85,7 @@ public class HgModifyCommand implements ModifyCommand { } } - private List pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy workingCopy) { + private List pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy workingCopy) { try { com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); workdirFactory.configure(pullCommand); 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 c80699add8..6286269d02 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 @@ -61,7 +61,8 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider Command.CAT, Command.DIFF, Command.LOG, - Command.TAGS, + Command.TAGS, + Command.BRANCH, Command.BRANCHES, Command.INCOMING, Command.OUTGOING, diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java index 515ee62c98..2920abc422 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java @@ -4,6 +4,6 @@ import com.aragost.javahg.Repository; import com.aragost.javahg.commands.PullCommand; import sonia.scm.repository.util.WorkdirFactory; -public interface HgWorkdirFactory extends WorkdirFactory { +public interface HgWorkdirFactory extends WorkdirFactory { void configure(PullCommand pullCommand); } 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 619f8b0892..412e9b7bf6 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 @@ -16,7 +16,7 @@ import java.io.IOException; import java.util.Map; import java.util.function.BiConsumer; -public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory implements HgWorkdirFactory { +public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory implements HgWorkdirFactory { private final Provider hgRepositoryEnvironmentBuilder; @@ -26,7 +26,7 @@ public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory cloneRepository(HgCommandContext context, File target, String initialBranch) throws IOException { + public ParentAndClone cloneRepository(HgCommandContext context, File target, String initialBranch) throws IOException { BiConsumer> repositoryMapBiConsumer = (repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment); Repository centralRepository = context.openWithSpecialEnvironment(repositoryMapBiConsumer); @@ -46,6 +46,11 @@ public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory b.getName().equals(branchToBeClosed)).isEmpty(); + } + + @Test + public void shouldThrowInternalRepositoryException() { + String branchToBeClosed = "default"; + + new HgBranchCommand(cmdContext, repository, workdirFactory).deleteOrClose(branchToBeClosed); + assertThrows(InternalRepositoryException.class, () -> new HgBranchCommand(cmdContext, repository, workdirFactory).deleteOrClose(branchToBeClosed)); + } + private List readBranches() { return new HgBranchesCommand(cmdContext, repository).getBranches(); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index 32b536e69d..2116d06a7a 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -33,6 +33,7 @@ package sonia.scm.repository.spi; +import com.aragost.javahg.commands.LogCommand; import org.junit.Test; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; @@ -40,6 +41,7 @@ import sonia.scm.repository.FileObject; import java.io.IOException; import java.util.Collection; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -79,6 +81,19 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertEquals("c", c.getPath()); } + @Test + public void testBrowseShouldResolveBranchForRevision() throws IOException { + String defaultBranchRevision = new LogCommand(cmdContext.open()).rev("default").single().getNode(); + + BrowseCommandRequest browseCommandRequest = new BrowseCommandRequest(); + browseCommandRequest.setRevision("default"); + + BrowserResult result = new HgBrowseCommand(cmdContext, + repository).getBrowserResult(browseCommandRequest); + + assertThat(result.getRevision()).isEqualTo(defaultBranchRevision); + } + @Test public void testBrowseSubDirectory() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java index 1daa395e07..07e9a0dec3 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java @@ -154,7 +154,7 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase HgLogCommand command = createComamnd(); String revision = "a9bacaf1b7fa0cebfca71fed4e59ed69a6319427"; Changeset c = - command.getChangeset(revision); + command.getChangeset(revision, null); assertNotNull(c); assertEquals(revision, c.getId()); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java new file mode 100644 index 0000000000..6ae7715ded --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnWorkDirFactory.java @@ -0,0 +1,9 @@ +package sonia.scm.repository; + +import sonia.scm.repository.spi.SvnContext; +import sonia.scm.repository.util.WorkdirFactory; + +import java.io.File; + +public interface SvnWorkDirFactory extends WorkdirFactory { +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java new file mode 100644 index 0000000000..bb9976b4a9 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactory.java @@ -0,0 +1,63 @@ +package sonia.scm.repository.spi; + +import org.apache.commons.lang.exception.CloneFailedException; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNURL; +import org.tmatesoft.svn.core.wc2.SvnCheckout; +import org.tmatesoft.svn.core.wc2.SvnOperationFactory; +import org.tmatesoft.svn.core.wc2.SvnTarget; +import sonia.scm.repository.Repository; +import sonia.scm.repository.SvnWorkDirFactory; +import sonia.scm.repository.util.SimpleWorkdirFactory; +import sonia.scm.repository.util.WorkdirProvider; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; + +public class SimpleSvnWorkDirFactory extends SimpleWorkdirFactory implements SvnWorkDirFactory { + + @Inject + public SimpleSvnWorkDirFactory(WorkdirProvider workdirProvider) { + super(workdirProvider); + } + + @Override + protected Repository getScmRepository(SvnContext context) { + return context.getRepository(); + } + + @Override + protected ParentAndClone cloneRepository(SvnContext context, File workingCopy, String initialBranch) { + + final SvnOperationFactory svnOperationFactory = new SvnOperationFactory(); + + SVNURL source; + try { + source = SVNURL.fromFile(context.getDirectory()); + } catch (SVNException ex) { + throw new CloneFailedException(ex.getMessage()); + } + + try { + final SvnCheckout checkout = svnOperationFactory.createCheckout(); + checkout.setSingleTarget(SvnTarget.fromFile(workingCopy)); + checkout.setSource(SvnTarget.fromURL(source)); + checkout.run(); + } catch (SVNException ex) { + throw new CloneFailedException(ex.getMessage()); + } finally { + svnOperationFactory.dispose(); + } + + return new ParentAndClone<>(context.getDirectory(), workingCopy); + } + + @Override + protected void closeRepository(File workingCopy) { + } + + @Override + protected void closeWorkdirInternal(File workdir) { + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java index 3a04ec1a2d..f8f6986c59 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnContext.java @@ -42,113 +42,57 @@ import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; +import sonia.scm.repository.Repository; import sonia.scm.repository.SvnUtil; //~--- JDK imports ------------------------------------------------------------ import java.io.Closeable; import java.io.File; -import java.io.IOException; /** * * @author Sebastian Sdorra */ -public class SvnContext implements Closeable -{ +public class SvnContext implements Closeable { - /** - * the logger for SvnContext - */ - private static final Logger logger = - LoggerFactory.getLogger(SvnContext.class); + private static final Logger LOG = LoggerFactory.getLogger(SvnContext.class); - //~--- constructors --------------------------------------------------------- + private final Repository repository; + private final File directory; - /** - * Constructs ... - * - * - * @param directory - */ - public SvnContext(File directory) - { + private SVNRepository svnRepository; + + public SvnContext(Repository repository, File directory) { + this.repository = repository; this.directory = directory; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @throws IOException - */ - @Override - public void close() throws IOException - { - if (logger.isTraceEnabled()) - { - logger.trace("close svn repository {}", directory); - } - - SvnUtil.closeSession(repository); - } - - /** - * Method description - * - * - * @return - * - * @throws SVNException - */ - public SVNURL createUrl() throws SVNException - { - return SVNURL.fromFile(directory); - } - - /** - * Method description - * - * - * @return - * - * @throws SVNException - */ - public SVNRepository open() throws SVNException - { - if (repository == null) - { - if (logger.isTraceEnabled()) - { - logger.trace("open svn repository {}", directory); - } - - repository = SVNRepositoryFactory.create(createUrl()); - } - + public Repository getRepository() { return repository; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public File getDirectory() - { + public File getDirectory() { return directory; } - //~--- fields --------------------------------------------------------------- + public SVNURL createUrl() throws SVNException { + return SVNURL.fromFile(directory); + } - /** Field description */ - private File directory; + public SVNRepository open() throws SVNException { + if (svnRepository == null) { + LOG.trace("open svn repository {}", directory); + svnRepository = SVNRepositoryFactory.create(createUrl()); + } + + return svnRepository; + } + + @Override + public void close() { + LOG.trace("close svn repository {}", directory); + SvnUtil.closeSession(svnRepository); + } - /** Field description */ - private SVNRepository repository; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java index be102be1bd..0cc8687154 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java @@ -75,7 +75,7 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand @Override @SuppressWarnings("unchecked") - public Changeset getChangeset(String revision) { + public Changeset getChangeset(String revision, LogCommandRequest request) { Changeset changeset = null; if (logger.isDebugEnabled()) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java new file mode 100644 index 0000000000..8475cf7c48 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModifyCommand.java @@ -0,0 +1,118 @@ +package sonia.scm.repository.spi; + +import org.tmatesoft.svn.core.SVNCommitInfo; +import org.tmatesoft.svn.core.SVNDepth; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.SVNWCClient; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.SvnWorkDirFactory; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +public class SvnModifyCommand implements ModifyCommand { + + private SvnContext context; + private SvnWorkDirFactory workDirFactory; + private Repository repository; + + SvnModifyCommand(SvnContext context, Repository repository, SvnWorkDirFactory workDirFactory) { + this.context = context; + this.repository = repository; + this.workDirFactory = workDirFactory; + } + + @Override + public String execute(ModifyCommandRequest request) { + SVNClientManager clientManager = SVNClientManager.newInstance(); + try (WorkingCopy workingCopy = workDirFactory.createWorkingCopy(context, null)) { + File workingDirectory = workingCopy.getDirectory(); + modifyWorkingDirectory(request, clientManager, workingDirectory); + return commitChanges(clientManager, workingDirectory, request.getCommitMessage()); + } + } + + private String commitChanges(SVNClientManager clientManager, File workingDirectory, String commitMessage) { + try { + SVNCommitInfo svnCommitInfo = clientManager.getCommitClient().doCommit( + new File[]{workingDirectory}, + false, + commitMessage, + null, + null, + false, + true, + SVNDepth.INFINITY + ); + return String.valueOf(svnCommitInfo.getNewRevision()); + } catch (SVNException e) { + throw new InternalRepositoryException(repository, "could not commit changes on repository"); + } + } + + private void modifyWorkingDirectory(ModifyCommandRequest request, SVNClientManager clientManager, File workingDirectory) { + for (ModifyCommandRequest.PartialRequest partialRequest : request.getRequests()) { + try { + SVNWCClient wcClient = clientManager.getWCClient(); + partialRequest.execute(new ModifyWorker(wcClient, workingDirectory)); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "could not read files from repository"); + } + } + } + + private class ModifyWorker implements ModifyWorkerHelper { + private final SVNWCClient wcClient; + private final File workingDirectory; + + private ModifyWorker(SVNWCClient wcClient, File workingDirectory) { + this.wcClient = wcClient; + this.workingDirectory = workingDirectory; + } + + @Override + public void doScmDelete(String toBeDeleted) { + try { + wcClient.doDelete(new File(workingDirectory, toBeDeleted), true, true, false); + } catch (SVNException e) { + throw new InternalRepositoryException(repository, "could not delete file from repository"); + } + } + + @Override + public void addFileToScm(String name, Path file) { + try { + wcClient.doAdd( + file.toFile(), + true, + false, + true, + SVNDepth.INFINITY, + false, + true + ); + } catch (SVNException e) { + throw new InternalRepositoryException(repository, "could not add file to repository"); + } + } + + @Override + public File getWorkDir() { + return workingDirectory; + } + + @Override + public Repository getRepository() { + return repository; + } + + @Override + public String getBranch() { + return null; + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java index ff277947bd..b12b787122 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java @@ -37,8 +37,10 @@ import com.google.common.collect.ImmutableSet; import com.google.common.io.Closeables; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.SvnWorkDirFactory; import sonia.scm.repository.api.Command; +import javax.inject.Inject; import java.io.IOException; import java.util.Set; @@ -53,17 +55,19 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider //J- public static final Set COMMANDS = ImmutableSet.of( Command.BLAME, Command.BROWSE, Command.CAT, Command.DIFF, - Command.LOG, Command.BUNDLE, Command.UNBUNDLE + Command.LOG, Command.BUNDLE, Command.UNBUNDLE, Command.MODIFY ); //J+ //~--- constructors --------------------------------------------------------- + @Inject SvnRepositoryServiceProvider(SvnRepositoryHandler handler, - Repository repository) + Repository repository, SvnWorkDirFactory workdirFactory) { this.repository = repository; - this.context = new SvnContext(handler.getDirectory(repository.getId())); + this.context = new SvnContext(repository, handler.getDirectory(repository.getId())); + this.workDirFactory = workdirFactory; } //~--- methods -------------------------------------------------------------- @@ -158,6 +162,10 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider return new SvnModificationsCommand(context, repository); } + public ModifyCommand getModifyCommand() { + return new SvnModifyCommand(context, repository, workDirFactory); + } + /** * Method description * @@ -189,4 +197,6 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider /** Field description */ private final Repository repository; + + private final SvnWorkDirFactory workDirFactory; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java index 763b5f445e..05097ff111 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java @@ -36,15 +36,18 @@ import com.google.inject.Inject; import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.SvnWorkDirFactory; @Extension public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { private SvnRepositoryHandler handler; + private SvnWorkDirFactory workdirFactory; @Inject - public SvnRepositoryServiceResolver(SvnRepositoryHandler handler) { + public SvnRepositoryServiceResolver(SvnRepositoryHandler handler, SvnWorkDirFactory workdirFactory) { this.handler = handler; + this.workdirFactory = workdirFactory; } @Override @@ -52,7 +55,7 @@ public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { SvnRepositoryServiceProvider provider = null; if (SvnRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new SvnRepositoryServiceProvider(handler, repository); + provider = new SvnRepositoryServiceProvider(handler, repository, workdirFactory); } return provider; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java index 8526d6380a..b4f0aaf920 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java @@ -38,6 +38,8 @@ import org.mapstruct.factory.Mappers; import sonia.scm.api.v2.resources.SvnConfigDtoToSvnConfigMapper; import sonia.scm.api.v2.resources.SvnConfigToSvnConfigDtoMapper; import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnWorkDirFactory; +import sonia.scm.repository.spi.SimpleSvnWorkDirFactory; /** * @@ -50,5 +52,6 @@ public class SvnServletModule extends ServletModule { protected void configureServlets() { bind(SvnConfigDtoToSvnConfigMapper.class).to(Mappers.getMapper(SvnConfigDtoToSvnConfigMapper.class).getClass()); bind(SvnConfigToSvnConfigDtoMapper.class).to(Mappers.getMapper(SvnConfigToSvnConfigDtoMapper.class).getClass()); + bind(SvnWorkDirFactory.class).to(SimpleSvnWorkDirFactory.class); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/AbstractSvnCommandTestBase.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/AbstractSvnCommandTestBase.java index 81b55e5db3..a0a9e9c77d 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/AbstractSvnCommandTestBase.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/AbstractSvnCommandTestBase.java @@ -72,7 +72,7 @@ public class AbstractSvnCommandTestBase extends ZippedRepositoryTestBase { if (context == null) { - context = new SvnContext(repositoryDirectory); + context = new SvnContext(repository, repositoryDirectory); } return context; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java new file mode 100644 index 0000000000..8beae9e0ed --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SimpleSvnWorkDirFactoryTest.java @@ -0,0 +1,77 @@ +package sonia.scm.repository.spi; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tmatesoft.svn.core.SVNException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SimpleSvnWorkDirFactoryTest extends AbstractSvnCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + // keep this so that it will not be garbage collected (Transport keeps this in a week reference) + private WorkdirProvider workdirProvider; + + @Before + public void initWorkDirProvider() throws IOException { + workdirProvider = new WorkdirProvider(temporaryFolder.newFolder()); + } + + @Test + public void shouldCheckoutLatestRevision() throws SVNException, IOException { + SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")) + .exists() + .isFile() + .hasContent("a and b\nline for blame test"); + } + } + + @Test + public void cloneFromPoolshouldNotBeReused() { + SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + + File firstDirectory; + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + firstDirectory = workingCopy.getDirectory(); + } + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + File secondDirectory = workingCopy.getDirectory(); + assertThat(secondDirectory).isNotEqualTo(firstDirectory); + } + } + + @Test + public void shouldDeleteCloneOnClose() { + SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + + File directory; + File workingRepository; + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + directory = workingCopy.getDirectory(); + workingRepository = workingCopy.getWorkingRepository(); + + } + assertThat(directory).doesNotExist(); + assertThat(workingRepository).doesNotExist(); + } + + @Test + public void shouldReturnRepository() { + SimpleSvnWorkDirFactory factory = new SimpleSvnWorkDirFactory(workdirProvider); + Repository scmRepository = factory.getScmRepository(createContext()); + assertThat(scmRepository).isSameAs(repository); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java index 0cfeaa3a1c..15198a19ed 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java @@ -128,7 +128,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase @Test public void testGetCommit() { - Changeset c = createCommand().getChangeset("3"); + Changeset c = createCommand().getChangeset("3", null); assertNotNull(c); assertEquals("3", c.getId()); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java new file mode 100644 index 0000000000..456971d9a2 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnModifyCommandTest.java @@ -0,0 +1,89 @@ +package sonia.scm.repository.spi; + +import org.junit.Before; +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; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SvnModifyCommandTest extends AbstractSvnCommandTestBase { + + private SvnModifyCommand svnModifyCommand; + private SvnContext context; + private SimpleSvnWorkDirFactory workDirFactory; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void initSvnModifyCommand() { + context = createContext(); + workDirFactory = new SimpleSvnWorkDirFactory(new WorkdirProvider(context.getDirectory())); + svnModifyCommand = new SvnModifyCommand(context, createRepository(), 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")); + + svnModifyCommand.execute(request); + WorkingCopy workingCopy = workDirFactory.createWorkingCopy(context, null); + assertThat(new File(workingCopy.getWorkingRepository().getAbsolutePath() + "/a.txt")).doesNotExist(); + assertThat(new File(workingCopy.getWorkingRepository().getAbsolutePath() + "/c")).exists(); + } + + @Test + public void shouldAddNewFile() throws IOException { + File testfile = temporaryFolder.newFile("Test123"); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Test123", testfile, false)); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + svnModifyCommand.execute(request); + + WorkingCopy workingCopy = workDirFactory.createWorkingCopy(context, null); + assertThat(new File(workingCopy.getWorkingRepository(), "Test123")).exists(); + } + + @Test + public void shouldThrowFileAlreadyExistsException() throws IOException { + File testfile = temporaryFolder.newFile("a.txt"); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", testfile, false)); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + assertThrows(AlreadyExistsException.class, () -> svnModifyCommand.execute(request)); + } + + @Test + public void shouldUpdateExistingFile() throws IOException { + File testfile = temporaryFolder.newFile("a.txt"); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", testfile, true)); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + svnModifyCommand.execute(request); + + WorkingCopy workingCopy = workDirFactory.createWorkingCopy(context, null); + assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")).exists(); + assertThat(new File(workingCopy.getWorkingRepository(), "a.txt")).hasContent(""); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java index 133518e8db..283a65449b 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java @@ -116,6 +116,6 @@ public class SvnUnbundleCommandTest extends AbstractSvnCommandTestBase SVNRepositoryFactory.createLocalRepository(folder, true, true); - return new SvnContext(folder); + return new SvnContext(repository, folder); } } diff --git a/scm-test/src/main/java/sonia/scm/util/MockUtil.java b/scm-test/src/main/java/sonia/scm/util/MockUtil.java index 415fefd620..4345b4d225 100644 --- a/scm-test/src/main/java/sonia/scm/util/MockUtil.java +++ b/scm-test/src/main/java/sonia/scm/util/MockUtil.java @@ -212,8 +212,8 @@ public final class MockUtil { SCMContextProvider provider = mock(SCMContextProvider.class); - when(provider.getBaseDirectory()).thenReturn(directory); - when(provider.resolve(any(Path.class))).then(ic -> { + lenient().when(provider.getBaseDirectory()).thenReturn(directory); + lenient().when(provider.resolve(any(Path.class))).then(ic -> { Path p = ic.getArgument(0); return directory.toPath().resolve(p); }); diff --git a/scm-ui/ui-components/src/Breadcrumb.tsx b/scm-ui/ui-components/src/Breadcrumb.tsx index 8402bc25eb..5fb34b79da 100644 --- a/scm-ui/ui-components/src/Breadcrumb.tsx +++ b/scm-ui/ui-components/src/Breadcrumb.tsx @@ -11,10 +11,10 @@ type Props = WithTranslation & { repository: Repository; branch: Branch; defaultBranch: Branch; - branches: Branch[]; revision: string; path: string; baseUrl: string; + sources: File; }; const FlexStartNav = styled.nav` @@ -59,7 +59,12 @@ class Breadcrumb extends React.Component { } render() { - const { baseUrl, branch, defaultBranch, branches, revision, path, repository, t } = this.props; + const { repository, baseUrl, branch, defaultBranch, sources, revision, path, t } = this.props; + + let homeUrl = baseUrl + "/"; + if (revision) { + homeUrl += encodeURIComponent(revision) + "/"; + } return ( <> @@ -67,7 +72,7 @@ class Breadcrumb extends React.Component {
  • - +
  • @@ -80,9 +85,10 @@ class Breadcrumb extends React.Component { name="repos.sources.actionbar" props={{ baseUrl, + revision, branch: branch ? branch : defaultBranch, path, - isBranchUrl: branches && branches.filter(b => b.name.replace("/", "%2F") === revision).length > 0, + sources, repository }} renderAll={true} diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 8ae5c405af..e156cc903e 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -113,7 +113,10 @@ "description": "Beschreibung", "size": "Größe" }, - "noSources": "Keine Sources in diesem Branch gefunden." + "noSources": "Keine Sources in diesem Branch gefunden.", + "extension" : { + "notBound": "Keine Erweiterung angebunden." + } }, "permission": { "title": "Berechtigungen bearbeiten", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index d621bad7bf..0466bab77f 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -113,7 +113,10 @@ "description": "Description", "size": "Size" }, - "noSources": "No sources found for this branch." + "noSources": "No sources found for this branch.", + "extension" : { + "notBound": "No extension bound." + } }, "permission": { "title": "Edit Permissions", diff --git a/scm-ui/ui-webapp/public/locales/es/repos.json b/scm-ui/ui-webapp/public/locales/es/repos.json index 8372c7e592..ef9bd2badc 100644 --- a/scm-ui/ui-webapp/public/locales/es/repos.json +++ b/scm-ui/ui-webapp/public/locales/es/repos.json @@ -113,7 +113,10 @@ "description": "Discripción", "size": "tamaño" }, - "noSources": "No se han encontrado fuentes para esta rama." + "noSources": "No se han encontrado fuentes para esta rama.", + "extension" : { + "notBound": "Sin extensión conectada." + } }, "permission": { "title": "Editar permisos", diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index ba974b3dfa..d65f2c9892 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -20,6 +20,7 @@ import PermissionsNavLink from "../components/PermissionsNavLink"; import Sources from "../sources/containers/Sources"; import RepositoryNavLink from "../components/RepositoryNavLink"; import { getLinks, getRepositoriesLink } from "../../modules/indexResource"; +import SourceExtensions from "../sources/containers/SourceExtensions"; type Props = WithTranslation & { namespace: string; @@ -67,6 +68,12 @@ class RepositoryRoot extends React.Component { return route.location.pathname.match(regex); }; + matchesSources = (route: any) => { + const url = this.matchedUrl(); + const regex = new RegExp(`${url}(/sources|/sourceext)/.*`); + return route.location.pathname.match(regex); + }; + render() { const { loading, error, indexLinks, repository, t } = this.props; @@ -120,6 +127,15 @@ class RepositoryRoot extends React.Component { path={`${url}/sources/:revision/:path*`} render={() => } /> + } + /> + } + /> ( @@ -186,6 +202,7 @@ class RepositoryRoot extends React.Component { to={`${url}/sources`} icon="fas fa-code" label={t("repositoryRoot.menu.sourcesNavLink")} + activeWhenMatch={this.matchesSources} activeOnlyWhenExact={false} /> diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx index 59f93e8453..adb56ecd97 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx @@ -75,7 +75,7 @@ class Content extends React.Component { }; showHeader() { - const { file, revision } = this.props; + const { repository, file, revision } = this.props; const { showHistory, collapsed } = this.state; const icon = collapsed ? "angle-right" : "angle-down"; @@ -99,6 +99,7 @@ class Content extends React.Component { void; +}; + +const extensionPointName = "repos.sources.extensions"; + +class SourceExtensions extends React.Component { + componentDidMount() { + const { fetchSources, repository, revision, path } = this.props; + // TODO get typing right + fetchSources(repository,revision || "", path || ""); + } + + render() { + const { loading, error, repository, extension, revision, path, sources, t } = this.props; + if (error) { + return ; + } + if (loading) { + return ; + } + + const extprops = { extension, repository, revision, path, sources }; + if (!binder.hasExtension(extensionPointName, extprops)) { + return {t("sources.extension.notBound")}; + } + + return ; + } +} + +const mapStateToProps = (state: any, ownProps: Props): Partial => { + const { repository, match } = ownProps; + // @ts-ignore + const revision: string = match.params.revision; + // @ts-ignore + const path: string = match.params.path; + // @ts-ignore + const extension: string = match.params.extension; + const loading = isFetchSourcesPending(state, repository, revision, path); + const error = getFetchSourcesFailure(state, repository, revision, path); + const sources = getSources(state, repository, revision, path); + + return { + repository, + extension, + revision, + path, + loading, + error, + sources + }; +}; + +const mapDispatchToProps = (dispatch: any) => { + return { + fetchSources: (repository: Repository, revision: string, path: string) => { + dispatch(fetchSources(repository, decodeURIComponent(revision), path)); + } + }; +}; + +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(withTranslation("repos")(SourceExtensions)) +); diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx index 33626ddcc4..bdf654b08a 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx @@ -13,7 +13,7 @@ import { } from "../../branches/modules/branches"; import { compose } from "redux"; import Content from "./Content"; -import { fetchSources, isDirectory } from "../modules/sources"; +import {fetchSources, getSources, isDirectory} from "../modules/sources"; type Props = WithTranslation & { repository: Repository; @@ -24,6 +24,7 @@ type Props = WithTranslation & { revision: string; path: string; currentFileIsDirectory: boolean; + sources: File; // dispatch props fetchBranches: (p: Repository) => void; @@ -52,7 +53,7 @@ class Sources extends React.Component { const { fetchBranches, repository, revision, path, fetchSources } = this.props; fetchBranches(repository); - fetchSources(repository, revision, path); + fetchSources(repository, this.decodeRevision(revision), path); this.redirectToDefaultBranch(); } @@ -60,12 +61,16 @@ class Sources extends React.Component { componentDidUpdate(prevProps) { const { fetchSources, repository, revision, path } = this.props; if (prevProps.revision !== revision || prevProps.path !== path) { - fetchSources(repository, revision, path); + fetchSources(repository, this.decodeRevision(revision), path); } this.redirectToDefaultBranch(); } + decodeRevision = (revision: string) => { + return revision ? decodeURIComponent(revision) : revision; + }; + redirectToDefaultBranch = () => { const { branches } = this.props; if (this.shouldRedirectToDefaultBranch()) { @@ -148,23 +153,22 @@ class Sources extends React.Component { }; renderBreadcrumb = () => { - const { revision, path, baseUrl, branches, repository } = this.props; + const { revision, path, baseUrl, branches, sources, repository } = this.props; const { selectedBranch } = this.state; - if (revision) { - return ( - b.defaultBranch === true)[0]} - branches={branches} - repository={repository} - /> - ); - } - return null; + return ( + b.defaultBranch === true)[0] + } + sources={sources} + /> + ); }; } @@ -178,15 +182,17 @@ const mapStateToProps = (state, ownProps) => { const currentFileIsDirectory = decodedRevision ? isDirectory(state, repository, decodedRevision, path) : isDirectory(state, repository, revision, path); + const sources = getSources(state, repository, decodedRevision, path); return { repository, - revision: decodedRevision, + revision, path, loading, error, branches, - currentFileIsDirectory + currentFileIsDirectory, + sources }; }; diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 8fbd45f864..826b32f1f6 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -1,5 +1,5 @@ import * as types from "../../../modules/types"; -import { Repository, File, Action } from "@scm-manager/ui-types"; +import { Repository, File, Action, Link } from "@scm-manager/ui-types"; import { apiClient } from "@scm-manager/ui-components"; import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; @@ -25,7 +25,7 @@ export function fetchSources(repository: Repository, revision: string, path: str } function createUrl(repository: Repository, revision: string, path: string) { - const base = repository._links.sources.href; + const base = (repository._links.sources as Link).href; if (!revision && !path) { return base; } @@ -61,7 +61,7 @@ export function fetchSourcesFailure(repository: Repository, revision: string, pa function createItemId(repository: Repository, revision: string, path: string) { const revPart = revision ? revision : "_"; const pathPart = path ? path : ""; - return `${repository.namespace}/${repository.name}/${revPart}/${pathPart}`; + return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}`; } // reducer diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java new file mode 100644 index 0000000000..bb74bc5045 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java @@ -0,0 +1,55 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.annotations.VisibleForTesting; +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.Links; +import org.mapstruct.Context; +import org.mapstruct.MapperConfig; +import org.mapstruct.ObjectFactory; +import sonia.scm.repository.BrowserResult; +import sonia.scm.repository.FileObject; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.SubRepository; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Link.link; + +@MapperConfig +abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements InstantAttributeMapper { + + @Inject + private ResourceLinks resourceLinks; + + @VisibleForTesting + void setResourceLinks(ResourceLinks resourceLinks) { + this.resourceLinks = resourceLinks; + } + + abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); + + @ObjectFactory + FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, FileObject fileObject) { + String path = removeFirstSlash(fileObject.getPath()); + Links.Builder links = Links.linkingTo(); + if (fileObject.isDirectory()) { + links.self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)); + } else { + links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)); + links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); + } + + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(links, embeddedBuilder, namespaceAndName, browserResult, fileObject); + + return new FileObjectDto(links.build(), embeddedBuilder.build()); + } + + abstract void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject); + + private String removeFirstSlash(String source) { + return source.startsWith("/") ? source.substring(1) : source; + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java index 588a8b3b2f..b8e34f8101 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java @@ -1,22 +1,60 @@ package sonia.scm.api.v2.resources; +import com.google.common.annotations.VisibleForTesting; +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.Links; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Qualifier; import sonia.scm.repository.BrowserResult; +import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; import javax.inject.Inject; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -public class BrowserResultToFileObjectDtoMapper { - - private final FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper; +@Mapper +public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectDtoMapper { @Inject - public BrowserResultToFileObjectDtoMapper(FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper) { - this.fileObjectToFileObjectDtoMapper = fileObjectToFileObjectDtoMapper; + private FileObjectToFileObjectDtoMapper childrenMapper; + + @VisibleForTesting + void setChildrenMapper(FileObjectToFileObjectDtoMapper childrenMapper) { + this.childrenMapper = childrenMapper; } - public FileObjectDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName) { - FileObjectDto fileObjectDto = fileObjectToFileObjectDtoMapper.map(browserResult.getFile(), namespaceAndName, browserResult); - fileObjectDto.setRevision( browserResult.getRevision() ); + FileObjectDto map(BrowserResult browserResult, @Context NamespaceAndName namespaceAndName) { + FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult); + fileObjectDto.setRevision(browserResult.getRevision()); return fileObjectDto; } + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + @Mapping(target = "children", qualifiedBy = Children.class) + protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult); + + @Children + protected FileObjectDto childrenToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult) { + return childrenMapper.map(fileObject, namespaceAndName, browserResult); + } + + @Override + void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) { + EdisonHalAppender appender = new EdisonHalAppender(links, embeddedBuilder); + // we call enrichers, which are only responsible for top level browseresults + applyEnrichers(appender, browserResult, namespaceAndName); + // we call enrichers, which are responsible for all file object top level browse result and its children + applyEnrichers(appender, fileObject, namespaceAndName, browserResult, browserResult.getRevision()); + } + + @Qualifier + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.CLASS) + @interface Children { + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java index 42da3e90c8..c2884a460f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java @@ -5,46 +5,18 @@ import de.otto.edison.hal.Links; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.ObjectFactory; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.SubRepository; - -import javax.inject.Inject; - -import static de.otto.edison.hal.Embedded.embeddedBuilder; -import static de.otto.edison.hal.Link.link; @Mapper -public abstract class FileObjectToFileObjectDtoMapper extends HalAppenderMapper implements InstantAttributeMapper { - - @Inject - private ResourceLinks resourceLinks; +public abstract class FileObjectToFileObjectDtoMapper extends BaseFileObjectDtoMapper { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes protected abstract FileObjectDto map(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult); - abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); - - @ObjectFactory - FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, FileObject fileObject) { - String path = removeFirstSlash(fileObject.getPath()); - Links.Builder links = Links.linkingTo(); - if (fileObject.isDirectory()) { - links.self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)); - } else { - links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)); - links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); - } - - Embedded.Builder embeddedBuilder = embeddedBuilder(); + @Override + void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) { applyEnrichers(new EdisonHalAppender(links, embeddedBuilder), fileObject, namespaceAndName, browserResult, browserResult.getRevision()); - - return new FileObjectDto(links.build(), embeddedBuilder.build()); - } - - private String removeFirstSlash(String source) { - return source.startsWith("/") ? source.substring(1) : source; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 0b419cf542..48de63969d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -38,6 +38,7 @@ public class MapperModule extends AbstractModule { bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass()); bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass()); + bind(BrowserResultToFileObjectDtoMapper.class).to(Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class).getClass()); bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass()); bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass()); @@ -46,8 +47,6 @@ public class MapperModule extends AbstractModule { bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass()); bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass()); - bind(MergeResultToDtoMapper.class).to(Mappers.getMapper(MergeResultToDtoMapper.class).getClass()); - // no mapstruct required bind(MeDtoFactory.class); bind(UIPluginDtoMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java deleted file mode 100644 index df59ba2abc..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java +++ /dev/null @@ -1,95 +0,0 @@ -package sonia.scm.api.v2.resources; - -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import lombok.extern.slf4j.Slf4j; -import org.apache.http.HttpStatus; -import sonia.scm.ConcurrentModificationException; -import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.RepositoryPermissions; -import sonia.scm.repository.api.MergeCommandBuilder; -import sonia.scm.repository.api.MergeCommandResult; -import sonia.scm.repository.api.MergeDryRunCommandResult; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.repository.api.RepositoryServiceFactory; -import sonia.scm.web.VndMediaType; - -import javax.inject.Inject; -import javax.validation.Valid; -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Response; - -@Slf4j -public class MergeResource { - - private final RepositoryServiceFactory serviceFactory; - private final MergeResultToDtoMapper mapper; - - @Inject - public MergeResource(RepositoryServiceFactory serviceFactory, MergeResultToDtoMapper mapper) { - this.serviceFactory = serviceFactory; - this.mapper = mapper; - } - - @POST - @Path("") - @Produces(VndMediaType.MERGE_RESULT) - @Consumes(VndMediaType.MERGE_COMMAND) - @StatusCodes({ - @ResponseCode(code = 204, condition = "merge has been executed successfully"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the privilege to write the repository"), - @ResponseCode(code = 409, condition = "The branches could not be merged automatically due to conflicts (conflicting files will be returned)"), - @ResponseCode(code = 500, condition = "internal server error") - }) - public Response merge(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) { - NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); - log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision()); - try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { - RepositoryPermissions.push(repositoryService.getRepository()).check(); - MergeCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).executeMerge(); - if (mergeCommandResult.isSuccess()) { - return Response.noContent().build(); - } else { - return Response.status(HttpStatus.SC_CONFLICT).entity(mapper.map(mergeCommandResult)).build(); - } - } - } - - @POST - @Path("dry-run/") - @StatusCodes({ - @ResponseCode(code = 204, condition = "merge can be done automatically"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 409, condition = "The branches can not be merged automatically due to conflicts"), - @ResponseCode(code = 500, condition = "internal server error") - }) - public Response dryRun(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) { - - NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); - log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision()); - try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { - if (RepositoryPermissions.push(repositoryService.getRepository()).isPermitted()) { - MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun(); - if (mergeCommandResult.isMergeable()) { - return Response.noContent().build(); - } else { - throw new ConcurrentModificationException("revision", mergeCommand.getTargetRevision()); - } - } else { - return Response.noContent().build(); - } - } - } - - private MergeCommandBuilder createMergeCommand(MergeCommandDto mergeCommand, RepositoryService repositoryService) { - return repositoryService - .getMergeCommand() - .setBranchToMerge(mergeCommand.getSourceRevision()) - .setTargetBranch(mergeCommand.getTargetRevision()); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultDto.java deleted file mode 100644 index fa523153cf..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package sonia.scm.api.v2.resources; - -import lombok.Getter; -import lombok.Setter; - -import java.util.Collection; - -@Getter -@Setter -public class MergeResultDto { - private Collection filesWithConflict; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultToDtoMapper.java deleted file mode 100644 index 1dbbe8aacd..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResultToDtoMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package sonia.scm.api.v2.resources; - -import org.mapstruct.Mapper; -import sonia.scm.repository.api.MergeCommandResult; - -@Mapper -public interface MergeResultToDtoMapper { - MergeResultDto map(MergeCommandResult result); -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 1cfc8b332b..2294dc600e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -42,7 +42,6 @@ public class RepositoryResource { private final Provider diffRootResource; private final Provider modificationsRootResource; private final Provider fileHistoryRootResource; - private final Provider mergeResource; private final Provider incomingRootResource; @Inject @@ -57,8 +56,8 @@ public class RepositoryResource { Provider diffRootResource, Provider modificationsRootResource, Provider fileHistoryRootResource, - Provider incomingRootResource, - Provider mergeResource) { + Provider incomingRootResource + ) { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; @@ -72,8 +71,8 @@ public class RepositoryResource { this.diffRootResource = diffRootResource; this.modificationsRootResource = modificationsRootResource; this.fileHistoryRootResource = fileHistoryRootResource; - this.mergeResource = mergeResource; this.incomingRootResource = incomingRootResource; + } /** @@ -208,9 +207,6 @@ public class RepositoryResource { return incomingRootResource.get(); } - @Path("merge/") - public MergeResource merge() {return mergeResource.get(); } - private Supplier loadBy(String namespace, String name) { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 67a6f26de5..c09f90c83d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -63,12 +63,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper fileHistoryRootResource; protected Provider repositoryCollectionResource; protected Provider incomingRootResource; - protected Provider mergeResource; RepositoryRootResource getRepositoryRootResource() { @@ -39,8 +38,7 @@ public abstract class RepositoryTestBase { diffRootResource, modificationsRootResource, fileHistoryRootResource, - incomingRootResource, - mergeResource)), repositoryCollectionResource); + incomingRootResource)), repositoryCollectionResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 478b3efc92..672771aadd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -45,7 +45,6 @@ public class ResourceLinksMock { when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo)); when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo)); when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo)); - when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo)); when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(uriInfo)); when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo)); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 542c0d017f..1a45d3b233 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -7,6 +7,7 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mapstruct.factory.Mappers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -49,7 +50,9 @@ public class SourceRootResourceTest extends RepositoryTestBase { @Before public void prepareEnvironment() throws Exception { - browserResultToFileObjectDtoMapper = new BrowserResultToFileObjectDtoMapper(fileObjectToFileObjectDtoMapper); + browserResultToFileObjectDtoMapper = Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class); + browserResultToFileObjectDtoMapper.setChildrenMapper(fileObjectToFileObjectDtoMapper); + browserResultToFileObjectDtoMapper.setResourceLinks(resourceLinks); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(service.getBrowseCommand()).thenReturn(browseCommandBuilder);