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/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/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..73159da5c1 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(); 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..fe39006a66 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 @@ -58,7 +58,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman @Override public Branch branch(BranchRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, request.getParentBranch())) { + 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(); 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/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 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-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..e41dbf96da 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 @@ -59,7 +59,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); @@ -83,7 +83,7 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand { .execute(); } - private void pullNewBranchIntoCentralRepository(BranchRequest request, WorkingCopy workingCopy) { + private void pullNewBranchIntoCentralRepository(BranchRequest request, WorkingCopy workingCopy) { try { PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository()); workdirFactory.configure(pullCommand); 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/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 { +} 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..6b6846f039 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()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java index b5c7e205be..b431997462 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java @@ -14,6 +14,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; import java.io.IOException; +import java.net.URLDecoder; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -57,7 +58,7 @@ public class SourceRootResource { BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand(); browseCommand.setPath(path); if (revision != null && !revision.isEmpty()) { - browseCommand.setRevision(revision); + browseCommand.setRevision(URLDecoder.decode(revision, "UTF-8")); } browseCommand.setDisableCache(true); BrowserResult browserResult = browseCommand.getBrowserResult(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java index b0f0d00708..162a90d45a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java @@ -7,6 +7,7 @@ import org.apache.shiro.util.ThreadState; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mapstruct.factory.Mappers; import org.mockito.InjectMocks; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; @@ -21,7 +22,6 @@ import static org.mockito.MockitoAnnotations.initMocks; public class BrowserResultToFileObjectDtoMapperTest { private final URI baseUri = URI.create("http://example.com/base/"); - @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks @@ -39,7 +39,10 @@ public class BrowserResultToFileObjectDtoMapperTest { @Before public void init() { initMocks(this); - mapper = new BrowserResultToFileObjectDtoMapper(fileObjectToFileObjectDtoMapper); + mapper = Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class); + mapper.setChildrenMapper(fileObjectToFileObjectDtoMapper); + mapper.setResourceLinks(resourceLinks); + subjectThreadState.bind(); ThreadContext.bind(subject); 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);