diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java index e26b2eb5aa..0b7592b446 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommandRequest.java @@ -90,8 +90,10 @@ public final class DiffCommandRequest extends FileBaseCommandRequest @Override public boolean isValid() { - return !Strings.isNullOrEmpty(getPath()) - ||!Strings.isNullOrEmpty(getRevision()); + return (!Strings.isNullOrEmpty(getPath()) + ||!Strings.isNullOrEmpty(getRevision())) + && (Strings.isNullOrEmpty(getAncestorChangeset()) + || Strings.isNullOrEmpty(getMergeChangeset())); } //~--- set methods ---------------------------------------------------------- @@ -101,7 +103,7 @@ public final class DiffCommandRequest extends FileBaseCommandRequest * * * @param format format of the diff output - * + * * @since 1.34 */ public void setFormat(DiffFormat format) @@ -112,14 +114,19 @@ public final class DiffCommandRequest extends FileBaseCommandRequest public void setAncestorChangeset(String ancestorChangeset) { this.ancestorChangeset = ancestorChangeset; } -//~--- get methods ---------------------------------------------------------- + + public void setMergeChangeset(String mergeChangeset) { + this.mergeChangeset = mergeChangeset; + } + + //~--- get methods ---------------------------------------------------------- /** * Return the output format of the diff command. * * * @return output format - * + * * @since 1.34 */ public DiffFormat getFormat() @@ -130,10 +137,17 @@ public final class DiffCommandRequest extends FileBaseCommandRequest public String getAncestorChangeset() { return ancestorChangeset; } -//~--- fields --------------------------------------------------------------- + + public String getMergeChangeset() { + return mergeChangeset; + } + + //~--- fields --------------------------------------------------------------- /** diff format */ private DiffFormat format = DiffFormat.NATIVE; private String ancestorChangeset; + + private String mergeChangeset; } 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 73159da5c1..c705e36e12 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 @@ -151,6 +151,32 @@ class AbstractGitCommand } } + > R inCloneWithPostponedClose(Function workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch, WorkingCopyCloser closer) { + try { + WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, initialBranch); + closer.setWorkingCopy(workingCopy); + Repository repository = workingCopy.getWorkingRepository(); + logger.debug("cloned repository to folder {}", repository.getWorkTree()); + return workerSupplier.apply(new Git(repository)).run(); + } catch (IOException e) { + throw new InternalRepositoryException(context.getRepository(), "could not clone repository", e); + } + } + + static class WorkingCopyCloser { + private WorkingCopy workingCopy; + + private void setWorkingCopy(WorkingCopy workingCopy) { + this.workingCopy = workingCopy; + } + + public void close() { + if (workingCopy != null) { + workingCopy.close(); + } + } + } + ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException { ObjectId resolved = repository.resolve(revision); if (resolved == null) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java index 0204ca4e3c..b781176972 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -8,6 +8,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; import sonia.scm.repository.GitUtil; @@ -37,6 +38,17 @@ final class Differ implements AutoCloseable { private static Differ create(Repository repository, DiffCommandRequest request) throws IOException { RevWalk walk = new RevWalk(repository); + if (!Strings.isNullOrEmpty(request.getMergeChangeset())) + { + ObjectId otherRevision = repository.resolve(request.getMergeChangeset()); + RevTree tree = walk.parseCommit(otherRevision).getTree(); + TreeWalk treeWalk = new TreeWalk(repository); + treeWalk.addTree(tree); + treeWalk.addTree(new FileTreeIterator( repository )); + return new Differ(null, walk, treeWalk); + } else { + + ObjectId revision = repository.resolve(request.getRevision()); RevCommit commit = walk.parseCommit(revision); @@ -46,40 +58,32 @@ final class Differ implements AutoCloseable { treeWalk.reset(); treeWalk.setRecursive(true); - if (Util.isNotEmpty(request.getPath())) - { + if (Util.isNotEmpty(request.getPath())) { treeWalk.setFilter(PathFilter.create(request.getPath())); } - if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) - { + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) { ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision); RevTree tree = walk.parseCommit(ancestorId).getTree(); treeWalk.addTree(tree); - } - else if (commit.getParentCount() > 0) - { + } else if (commit.getParentCount() > 0) { RevTree tree = commit.getParent(0).getTree(); - if (tree != null) - { + if (tree != null) { treeWalk.addTree(tree); - } - else - { + } else { treeWalk.addTree(new EmptyTreeIterator()); } - } - else - { + } else { treeWalk.addTree(new EmptyTreeIterator()); } treeWalk.addTree(commit.getTree()); - return new Differ(commit, walk, treeWalk); + return new Differ(commit, walk, treeWalk); + } } private Diff diff() throws IOException { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 1ac64c1b5e..86c2c9baab 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -31,8 +31,14 @@ package sonia.scm.repository.spi; +import com.google.common.base.Strings; +import org.eclipse.jgit.api.MergeCommand; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.lib.ObjectId; +import sonia.scm.repository.GitWorkdirFactory; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.DiffCommandBuilder; @@ -44,15 +50,42 @@ import java.io.IOException; */ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { - GitDiffCommand(GitContext context, Repository repository) { + private final GitWorkdirFactory workdirFactory; + + GitDiffCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) { super(context, repository); + this.workdirFactory = workdirFactory; } @Override public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException { - @SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService - org.eclipse.jgit.lib.Repository repository = open(); + WorkingCopyCloser closer = new WorkingCopyCloser(); + if (Strings.isNullOrEmpty(request.getMergeChangeset())) { + return computeDiff(request, open(), closer); + } else { + return inCloneWithPostponedClose(git -> new GitCloneWorker(git) { + @Override + DiffCommandBuilder.OutputStreamConsumer run() throws IOException { + ObjectId sourceRevision = resolveRevision(request.getRevision()); + try { + getClone().merge() + .setFastForward(MergeCommand.FastForwardMode.NO_FF) + .setCommit(false) // we want to set the author manually + .include(request.getRevision(), sourceRevision) + .call(); + } catch (GitAPIException e) { + throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + request.getRevision() + " into " + request.getMergeChangeset(), e); + } + DiffCommandRequest clone = request.clone(); + clone.setRevision(sourceRevision.name()); + return computeDiff(request, getClone().getRepository(), closer); + } + }, workdirFactory, request.getMergeChangeset(), closer); + } + } + + private DiffCommandBuilder.OutputStreamConsumer computeDiff(DiffCommandRequest request, org.eclipse.jgit.lib.Repository repository, WorkingCopyCloser closer) throws IOException { Differ.Diff diff = Differ.diff(repository, request); return output -> { @@ -66,8 +99,9 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { } formatter.flush(); + } finally { + closer.close(); } }; } - } 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..2778fecf1d 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 @@ -169,7 +169,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public DiffCommand getDiffCommand() { - return new GitDiffCommand(context, repository); + return new GitDiffCommand(context, repository, handler.getWorkdirFactory()); } @Override diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index 52932e83ae..854a217f1c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -40,7 +40,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { @Test public void diffForOneRevisionShouldCreateDiff() throws IOException { - GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + GitDiffCommand gitDiffCommand = createDiffCommand(); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -50,7 +50,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { @Test public void diffForOneBranchShouldCreateDiff() throws IOException { - GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + GitDiffCommand gitDiffCommand = createDiffCommand(); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -60,7 +60,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { @Test public void diffForPathShouldCreateLimitedDiff() throws IOException { - GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + GitDiffCommand gitDiffCommand = createDiffCommand(); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); diffCommandRequest.setPath("a.txt"); @@ -71,7 +71,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { @Test public void diffBetweenTwoBranchesShouldCreateDiff() throws IOException { - GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + GitDiffCommand gitDiffCommand = createDiffCommand(); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("master"); diffCommandRequest.setAncestorChangeset("test-branch"); @@ -82,7 +82,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { @Test public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() throws IOException { - GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + GitDiffCommand gitDiffCommand = createDiffCommand(); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("master"); diffCommandRequest.setAncestorChangeset("test-branch"); @@ -91,4 +91,8 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString()); } + + private GitDiffCommand createDiffCommand() { + return new GitDiffCommand(createContext(), repository, null); + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_Merge_Test.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_Merge_Test.java new file mode 100644 index 0000000000..300fbee4dc --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommand_Merge_Test.java @@ -0,0 +1,58 @@ +package sonia.scm.repository.spi; + +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.repository.util.WorkdirProvider; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GitDiffCommand_Merge_Test extends AbstractGitCommandTestBase { + + static final String DIFF_HEADER = "diff --git a/Main.java b/Main.java"; + static final String DIFF_FILE_A_MULTIPLE_REVISIONS = "--- a/Main.java\n" + + "+++ b/Main.java\n" + + "@@ -1,5 +1,5 @@\n" + + " class Main {\n" + + "- public static void main(String[] args) {\n" + + "+ public static void main(String[] arguments) {\n" + + " System.out.println(\"Expect nothing more to happen.\");\n" + + " System.out.println(\"This is for demonstration, only.\");\n" + + " }\n"; + + @Rule + public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + + @Test + public void diffBetweenTwoBranchesWithoutConflict() throws IOException { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("feature/rename_variable"); + diffCommandRequest.setMergeChangeset("integration"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); + assertThat(output.toString()) + .contains(DIFF_HEADER) + .contains(DIFF_FILE_A_MULTIPLE_REVISIONS); + } + + @Test + public void diffBetweenTwoBranchesWithSimpleConflict() throws IOException { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider())); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("feature/print_args"); + diffCommandRequest.setMergeChangeset("integration"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); + assertThat(output.toString()) + .contains(DIFF_HEADER) + .contains(DIFF_FILE_A_MULTIPLE_REVISIONS); + } + + @Override + protected String getZippedRepositoryResource() { + return "sonia/scm/repository/spi/scm-git-spi-merge-diff-test.zip"; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-merge-diff-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-merge-diff-test.zip new file mode 100644 index 0000000000..9d957ef6f5 Binary files /dev/null and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-merge-diff-test.zip differ