permissions) {
+ this.permissions.clear();
+ this.permissions.addAll(permissions);
+ }
+
+ public void addPermission(Permission newPermission) {
+ this.permissions.add(newPermission);
+ }
+
+ public void removePermission(Permission permission) {
+ this.permissions.remove(permission);
}
public void setPublicReadable(boolean publicReadable) {
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java
index 445fc22ab8..79c06d03f9 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java
@@ -36,7 +36,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.Handler;
-import sonia.scm.NotSupportedFeatuerException;
+import sonia.scm.NotSupportedFeatureException;
import sonia.scm.plugin.ExtensionPoint;
/**
@@ -70,9 +70,9 @@ public interface RepositoryHandler
* @return {@link ImportHandler} for the repository type of this handler
* @since 1.12
*
- * @throws NotSupportedFeatuerException
+ * @throws NotSupportedFeatureException
*/
- public ImportHandler getImportHandler() throws NotSupportedFeatuerException;
+ public ImportHandler getImportHandler() throws NotSupportedFeatureException;
/**
* Returns informations about the version of the RepositoryHandler.
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
index 1e2fdccf42..7f4880c258 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
@@ -35,7 +35,6 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
-import sonia.scm.AlreadyExistsException;
import sonia.scm.TypeManager;
import java.io.IOException;
@@ -73,7 +72,7 @@ public interface RepositoryManager
*
* @throws IOException
*/
- public void importRepository(Repository repository) throws IOException, AlreadyExistsException;
+ public void importRepository(Repository repository) throws IOException;
//~--- get methods ----------------------------------------------------------
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
index 87df960da7..b7a819e5f9 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
@@ -35,7 +35,6 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
-import sonia.scm.AlreadyExistsException;
import sonia.scm.ManagerDecorator;
import sonia.scm.Type;
@@ -82,7 +81,7 @@ public class RepositoryManagerDecorator
* {@inheritDoc}
*/
@Override
- public void importRepository(Repository repository) throws IOException, AlreadyExistsException {
+ public void importRepository(Repository repository) throws IOException {
decorated.importRepository(repository);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java
deleted file mode 100644
index 9dd866daa4..0000000000
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.repository;
-
-import sonia.scm.NotFoundException;
-
-/**
- * Signals that the specified {@link Repository} could be found.
- *
- * @author Sebastian Sdorra
- * @since 1.6
- */
-public class RepositoryNotFoundException extends NotFoundException
-{
-
- private static final long serialVersionUID = -6583078808900520166L;
- private static final String TYPE_REPOSITORY = "repository";
-
- //~--- constructors ---------------------------------------------------------
-
- /**
- * Constructs a new {@link RepositoryNotFoundException} with null as its
- * error detail message.
- *
- */
- public RepositoryNotFoundException(Repository repository) {
- super(TYPE_REPOSITORY, repository.getName() + "/" + repository.getNamespace());
- }
-
- public RepositoryNotFoundException(String repositoryId) {
- super(TYPE_REPOSITORY, repositoryId);
- }
-
- public RepositoryNotFoundException(NamespaceAndName namespaceAndName) {
- super(TYPE_REPOSITORY, namespaceAndName.toString());
- }
-}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java
deleted file mode 100644
index 4185e3223d..0000000000
--- a/scm-core/src/main/java/sonia/scm/repository/RevisionNotFoundException.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.repository;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import sonia.scm.NotFoundException;
-import sonia.scm.util.Util;
-
-/**
- * Signals that the specified revision could be found.
- *
- * @author Sebastian Sdorra
- */
-public class RevisionNotFoundException extends NotFoundException {
-
- /** Field description */
- private static final long serialVersionUID = -5594008535358811998L;
-
- //~--- constructors ---------------------------------------------------------
-
- /**
- * Constructs a new {@link RevisionNotFoundException}
- * with the specified revision.
- *
- *
- * @param revision revision which could not be found
- */
- public RevisionNotFoundException(String revision)
- {
- super("revision", revision);
- this.revision = Util.nonNull(revision);
- }
-
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Return the revision which could not be found.
- *
- *
- * @return revision which could not be found
- */
- public String getRevision()
- {
- return revision;
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private String revision;
-}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
index d2db6856a7..d482c04ea4 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
@@ -38,7 +38,6 @@ package sonia.scm.repository.api;
import com.google.common.base.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import sonia.scm.NotFoundException;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.repository.BrowserResult;
@@ -46,7 +45,6 @@ import sonia.scm.repository.FileObject;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey;
-import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.BrowseCommand;
import sonia.scm.repository.spi.BrowseCommandRequest;
@@ -136,7 +134,7 @@ public final class BrowseCommandBuilder
*
* @throws IOException
*/
- public BrowserResult getBrowserResult() throws IOException, NotFoundException {
+ public BrowserResult getBrowserResult() throws IOException {
BrowserResult result = null;
if (disableCache)
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java
index bf896efed8..d1e0cbc5f1 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java
@@ -37,9 +37,7 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.Repository;
-import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.CatCommand;
import sonia.scm.repository.spi.CatCommandRequest;
import sonia.scm.util.IOUtil;
@@ -107,7 +105,7 @@ public final class CatCommandBuilder
* @param outputStream output stream for the content
* @param path file path
*/
- public void retriveContent(OutputStream outputStream, String path) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public void retriveContent(OutputStream outputStream, String path) throws IOException {
getCatResult(outputStream, path);
}
@@ -116,7 +114,7 @@ public final class CatCommandBuilder
*
* @param path file path
*/
- public InputStream getStream(String path) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public InputStream getStream(String path) throws IOException {
Preconditions.checkArgument(!Strings.isNullOrEmpty(path),
"path is required");
@@ -139,7 +137,7 @@ public final class CatCommandBuilder
*
* @throws IOException
*/
- public String getContent(String path) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public String getContent(String path) throws IOException {
String content = null;
ByteArrayOutputStream baos = null;
@@ -186,7 +184,7 @@ public final class CatCommandBuilder
* @throws IOException
*/
private void getCatResult(OutputStream outputStream, String path)
- throws IOException, PathNotFoundException, RevisionNotFoundException {
+ throws IOException {
Preconditions.checkNotNull(outputStream, "OutputStream is required");
Preconditions.checkArgument(!Strings.isNullOrEmpty(path),
"path is required");
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java
index 2f844cfbfb..fa09cea2cb 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java
@@ -66,6 +66,10 @@ public enum Command
/**
* @since 2.0
*/
- MODIFICATIONS
+ MODIFICATIONS,
+ /**
+ * @since 2.0
+ */
+ MERGE
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java
index c0e9e3f622..7217d0e97a 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java
@@ -38,7 +38,6 @@ package sonia.scm.repository.api;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.DiffCommand;
import sonia.scm.repository.spi.DiffCommandRequest;
import sonia.scm.util.IOUtil;
@@ -104,7 +103,7 @@ public final class DiffCommandBuilder
*
* @throws IOException
*/
- public DiffCommandBuilder retriveContent(OutputStream outputStream) throws IOException, RevisionNotFoundException {
+ public DiffCommandBuilder retrieveContent(OutputStream outputStream) throws IOException {
getDiffResult(outputStream);
return this;
@@ -119,7 +118,7 @@ public final class DiffCommandBuilder
*
* @throws IOException
*/
- public String getContent() throws IOException, RevisionNotFoundException {
+ public String getContent() throws IOException {
String content = null;
ByteArrayOutputStream baos = null;
@@ -199,7 +198,7 @@ public final class DiffCommandBuilder
*
* @throws IOException
*/
- private void getDiffResult(OutputStream outputStream) throws IOException, RevisionNotFoundException {
+ private void getDiffResult(OutputStream outputStream) throws IOException {
Preconditions.checkNotNull(outputStream, "OutputStream is required");
Preconditions.checkArgument(request.isValid(),
"path and/or revision is required");
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 9c782a781b..73062a0244 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
@@ -46,7 +46,6 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey;
-import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.LogCommand;
import sonia.scm.repository.spi.LogCommandRequest;
@@ -165,7 +164,7 @@ public final class LogCommandBuilder
*
* @throws IOException
*/
- public Changeset getChangeset(String id) throws IOException, RevisionNotFoundException {
+ public Changeset getChangeset(String id) throws IOException {
Changeset changeset;
if (disableCache)
@@ -224,7 +223,7 @@ public final class LogCommandBuilder
*
* @throws IOException
*/
- public ChangesetPagingResult getChangesets() throws IOException, RevisionNotFoundException {
+ public ChangesetPagingResult getChangesets() throws IOException {
ChangesetPagingResult cpr;
if (disableCache)
@@ -398,6 +397,11 @@ public final class LogCommandBuilder
return this;
}
+ public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) {
+ request.setAncestorChangeset(ancestorChangeset);
+ return this;
+ }
+
//~--- inner classes --------------------------------------------------------
/**
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
new file mode 100644
index 0000000000..881a374864
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java
@@ -0,0 +1,143 @@
+package sonia.scm.repository.api;
+
+import com.google.common.base.Preconditions;
+import sonia.scm.repository.Person;
+import sonia.scm.repository.spi.MergeCommand;
+import sonia.scm.repository.spi.MergeCommandRequest;
+
+/**
+ * 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
+ * the target branch ({@link #setTargetBranch(String)}) and the name of the branch that should be merged
+ * ({@link #setBranchToMerge(String)}). Additionally you can specify an author that should be used for the commit
+ * ({@link #setAuthor(Person)}) and a message template ({@link #setMessageTemplate(String)}) if you are not doing a dry
+ * run only. If no author is specified, the logged in user and a default message will be used instead.
+ *
+ * To actually merge feature_branch into integration_branch do this:
+ *
+ * repositoryService.gerMergeCommand()
+ * .setBranchToMerge("feature_branch")
+ * .setTargetBranch("integration_branch")
+ * .executeMerge();
+ *
+ *
+ * If the merge is successful, the result will look like this:
+ *
+ * O <- Merge result (new head of integration_branch)
+ * |\
+ * | \
+ * old integration_branch -> O O <- feature_branch
+ * | |
+ * O O
+ *
+ *
+ * To check whether they can be merged without conflicts beforehand do this:
+ *
+ * repositoryService.gerMergeCommand()
+ * .setBranchToMerge("feature_branch")
+ * .setTargetBranch("integration_branch")
+ * .dryRun()
+ * .isMergeable();
+ *
+ *
+ * Keep in mind that you should always check the result of a merge even though you may have done a dry run
+ * beforehand, because the branches can change between the dry run and the actual merge.
+ *
+ * @since 2.0.0
+ */
+public class MergeCommandBuilder {
+
+ private final MergeCommand mergeCommand;
+ private final MergeCommandRequest request = new MergeCommandRequest();
+
+ MergeCommandBuilder(MergeCommand mergeCommand) {
+ this.mergeCommand = mergeCommand;
+ }
+
+ /**
+ * Use this to set the branch that should be merged into the target branch.
+ *
+ * This is mandatory.
+ *
+ * @return This builder instance.
+ */
+ public MergeCommandBuilder setBranchToMerge(String branchToMerge) {
+ request.setBranchToMerge(branchToMerge);
+ return this;
+ }
+
+ /**
+ * Use this to set the target branch the other branch should be merged into.
+ *
+ * This is mandatory.
+ *
+ * @return This builder instance.
+ */
+ public MergeCommandBuilder setTargetBranch(String targetBranch) {
+ request.setTargetBranch(targetBranch);
+ return this;
+ }
+
+ /**
+ * Use this to set the author of the merge commit manually. If this is omitted, the currently logged in user will be
+ * used instead.
+ *
+ * This is optional and for {@link #executeMerge()} only.
+ *
+ * @return This builder instance.
+ */
+ public MergeCommandBuilder setAuthor(Person author) {
+ request.setAuthor(author);
+ return this;
+ }
+
+ /**
+ * Use this to set a template for the commit message. If no message is set, a default message will be used.
+ *
+ * You can use the placeholder {0} for the branch to be merged and {1} for the target
+ * branch, eg.:
+ *
+ *
+ * ...setMessageTemplate("Merge of {0} into {1}")...
+ *
+ *
+ * This is optional and for {@link #executeMerge()} only.
+ *
+ * @return This builder instance.
+ */
+ public MergeCommandBuilder setMessageTemplate(String messageTemplate) {
+ request.setMessageTemplate(messageTemplate);
+ return this;
+ }
+
+ /**
+ * Use this to reset the command.
+ * @return This builder instance.
+ */
+ public MergeCommandBuilder reset() {
+ request.reset();
+ return this;
+ }
+
+ /**
+ * Use this to actually do the merge. If an automatic merge is not possible, {@link MergeCommandResult#isSuccess()}
+ * will return false.
+ *
+ * @return The result of the merge.
+ */
+ public MergeCommandResult executeMerge() {
+ Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
+ return mergeCommand.merge(request);
+ }
+
+ /**
+ * Use this to check whether the given branches can be merged autmatically. If this is possible,
+ * {@link MergeDryRunCommandResult#isMergeable()} will return true.
+ *
+ * @return The result whether the given branches can be merged automatically.
+ */
+ public MergeDryRunCommandResult dryRun() {
+ Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
+ return mergeCommand.dryRun(request);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java
new file mode 100644
index 0000000000..53f712cddc
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java
@@ -0,0 +1,44 @@
+package sonia.scm.repository.api;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableCollection;
+
+/**
+ * This class keeps the result of a merge of branches. Use {@link #isSuccess()} to check whether the merge was
+ * sucessfully executed. If the result is false the merge could not be done without conflicts. In this
+ * case you can use {@link #getFilesWithConflict()} to get a list of files with merge conflicts.
+ */
+public class MergeCommandResult {
+ private final Collection filesWithConflict;
+
+ private MergeCommandResult(Collection filesWithConflict) {
+ this.filesWithConflict = filesWithConflict;
+ }
+
+ public static MergeCommandResult success() {
+ return new MergeCommandResult(emptyList());
+ }
+
+ public static MergeCommandResult failure(Collection filesWithConflict) {
+ return new MergeCommandResult(new HashSet<>(filesWithConflict));
+ }
+
+ /**
+ * If this returns true, the merge was successfull. If this returns false there were
+ * merge conflicts. In this case you can use {@link #getFilesWithConflict()} to check what files could not be merged.
+ */
+ public boolean isSuccess() {
+ return filesWithConflict.isEmpty();
+ }
+
+ /**
+ * If the merge was not successful ({@link #isSuccess()} returns false) this will give you a list of
+ * file paths that could not be merged automatically.
+ */
+ public Collection getFilesWithConflict() {
+ return unmodifiableCollection(filesWithConflict);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java
new file mode 100644
index 0000000000..6ebb330aae
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeDryRunCommandResult.java
@@ -0,0 +1,22 @@
+package sonia.scm.repository.api;
+
+/**
+ * This class keeps the result of a merge dry run. Use {@link #isMergeable()} to check whether an automatic merge is
+ * possible or not.
+ */
+public class MergeDryRunCommandResult {
+
+ private final boolean mergeable;
+
+ public MergeDryRunCommandResult(boolean mergeable) {
+ this.mergeable = mergeable;
+ }
+
+ /**
+ * This will return true, when an automatic merge is possible at the moment ; false
+ * otherwise.
+ */
+ public boolean isMergeable() {
+ return mergeable;
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java
index 6459b47cd5..498746cc60 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java
@@ -13,7 +13,6 @@ import sonia.scm.repository.Modifications;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey;
-import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.ModificationsCommand;
import sonia.scm.repository.spi.ModificationsCommandRequest;
@@ -67,7 +66,7 @@ public final class ModificationsCommandBuilder {
return this;
}
- public Modifications getModifications() throws IOException, RevisionNotFoundException {
+ public Modifications getModifications() throws IOException {
Modifications modifications;
if (disableCache) {
log.info("Get modifications for {} with disabled cache", request);
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
index bdd6e4b320..fe0529e6b5 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
@@ -79,6 +79,7 @@ import java.util.stream.Stream;
* @apiviz.uses sonia.scm.repository.api.PushCommandBuilder
* @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
+ * @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
* @since 1.17
*/
@Slf4j
@@ -353,6 +354,22 @@ public final class RepositoryService implements Closeable {
repository);
}
+ /**
+ * The merge command executes a merge of two branches. It is possible to do a dry run to check, whether the given
+ * branches can be merged without conflicts.
+ *
+ * @return instance of {@link MergeCommandBuilder}
+ * @throws CommandNotSupportedException if the command is not supported
+ * by the implementation of the repository service provider.
+ * @since 2.0.0
+ */
+ public MergeCommandBuilder gerMergeCommand() {
+ logger.debug("create unbundle command for repository {}",
+ repository.getNamespaceAndName());
+
+ return new MergeCommandBuilder(provider.getMergeCommand());
+ }
+
/**
* Returns true if the command is supported by the repository service.
*
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
index fbb1ee6b58..8db3ab7546 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
@@ -45,6 +45,7 @@ import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.HandlerEventType;
+import sonia.scm.NotFoundException;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration;
@@ -57,7 +58,6 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKeyPredicate;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryManager;
-import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.RepositoryServiceProvider;
import sonia.scm.repository.spi.RepositoryServiceResolver;
@@ -65,6 +65,9 @@ import sonia.scm.security.ScmSecurityException;
import java.util.Set;
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+import static sonia.scm.NotFoundException.notFound;
+
//~--- JDK imports ------------------------------------------------------------
/**
@@ -161,7 +164,7 @@ public final class RepositoryServiceFactory
* @return a implementation of RepositoryService
* for the given type of repository
*
- * @throws RepositoryNotFoundException if no repository
+ * @throws NotFoundException if no repository
* with the given id is available
* @throws RepositoryServiceNotFoundException if no repository service
* implementation for this kind of repository is available
@@ -169,7 +172,7 @@ public final class RepositoryServiceFactory
* @throws ScmSecurityException if current user has not read permissions
* for that repository
*/
- public RepositoryService create(String repositoryId) throws RepositoryNotFoundException {
+ public RepositoryService create(String repositoryId) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(repositoryId),
"a non empty repositoryId is required");
@@ -177,7 +180,7 @@ public final class RepositoryServiceFactory
if (repository == null)
{
- throw new RepositoryNotFoundException(repositoryId);
+ throw new NotFoundException(Repository.class, repositoryId);
}
return create(repository);
@@ -192,7 +195,7 @@ public final class RepositoryServiceFactory
* @return a implementation of RepositoryService
* for the given type of repository
*
- * @throws RepositoryNotFoundException if no repository
+ * @throws NotFoundException if no repository
* with the given id is available
* @throws RepositoryServiceNotFoundException if no repository service
* implementation for this kind of repository is available
@@ -201,7 +204,6 @@ public final class RepositoryServiceFactory
* for that repository
*/
public RepositoryService create(NamespaceAndName namespaceAndName)
- throws RepositoryNotFoundException
{
Preconditions.checkArgument(namespaceAndName != null,
"a non empty namespace and name is required");
@@ -210,7 +212,7 @@ public final class RepositoryServiceFactory
if (repository == null)
{
- throw new RepositoryNotFoundException(namespaceAndName);
+ throw notFound(entity(namespaceAndName));
}
return create(repository);
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java
index be679f9df1..ee37d6243e 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java
@@ -35,7 +35,6 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
-import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult;
import java.io.IOException;
@@ -60,5 +59,5 @@ public interface BrowseCommand
*
* @throws IOException
*/
- BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, NotFoundException;
+ BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java
index 06f242783b..81600230db 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java
@@ -33,9 +33,6 @@
package sonia.scm.repository.spi;
-import sonia.scm.repository.PathNotFoundException;
-import sonia.scm.repository.RevisionNotFoundException;
-
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -47,7 +44,7 @@ import java.io.OutputStream;
*/
public interface CatCommand {
- void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RevisionNotFoundException, PathNotFoundException;
+ void getCatResult(CatCommandRequest request, OutputStream output) throws IOException;
- InputStream getCatResultStream(CatCommandRequest request) throws IOException, RevisionNotFoundException, PathNotFoundException;
+ InputStream getCatResultStream(CatCommandRequest request) throws IOException;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java
index 105f4e48e1..bba42cd86d 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffCommand.java
@@ -33,8 +33,6 @@
package sonia.scm.repository.spi;
-import sonia.scm.repository.RevisionNotFoundException;
-
import java.io.IOException;
import java.io.OutputStream;
@@ -56,5 +54,5 @@ public interface DiffCommand
* @throws IOException
* @throws RuntimeException
*/
- public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException, RevisionNotFoundException;
+ public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException;
}
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 5cfd57f71b..e26b2eb5aa 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
@@ -109,7 +109,10 @@ public final class DiffCommandRequest extends FileBaseCommandRequest
this.format = format;
}
- //~--- get methods ----------------------------------------------------------
+ public void setAncestorChangeset(String ancestorChangeset) {
+ this.ancestorChangeset = ancestorChangeset;
+ }
+//~--- get methods ----------------------------------------------------------
/**
* Return the output format of the diff command.
@@ -124,8 +127,13 @@ public final class DiffCommandRequest extends FileBaseCommandRequest
return format;
}
- //~--- fields ---------------------------------------------------------------
+ public String getAncestorChangeset() {
+ return ancestorChangeset;
+ }
+//~--- fields ---------------------------------------------------------------
/** diff format */
private DiffFormat format = DiffFormat.NATIVE;
+
+ private String ancestorChangeset;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java b/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java
index 52ef68f272..4bbe61ea41 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java
@@ -40,10 +40,12 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.RepositoryManager;
-import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+import static sonia.scm.NotFoundException.notFound;
+
/**
*
* @author Sebastian Sdorra
@@ -71,18 +73,18 @@ public final class HookEventFacade
//~--- methods --------------------------------------------------------------
- public HookEventHandler handle(String id) throws RepositoryNotFoundException {
+ public HookEventHandler handle(String id) {
return handle(repositoryManagerProvider.get().get(id));
}
- public HookEventHandler handle(NamespaceAndName namespaceAndName) throws RepositoryNotFoundException {
+ public HookEventHandler handle(NamespaceAndName namespaceAndName) {
return handle(repositoryManagerProvider.get().get(namespaceAndName));
}
- public HookEventHandler handle(Repository repository) throws RepositoryNotFoundException {
+ public HookEventHandler handle(Repository repository) {
if (repository == null)
{
- throw new RepositoryNotFoundException(repository);
+ throw notFound(entity(repository));
}
return new HookEventHandler(repositoryManagerProvider.get(),
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 e4a7f3437b..f4babcee72 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
@@ -37,7 +37,6 @@ package sonia.scm.repository.spi;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
-import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
@@ -50,7 +49,7 @@ import java.io.IOException;
*/
public interface LogCommand {
- Changeset getChangeset(String id) throws IOException, RevisionNotFoundException;
+ Changeset getChangeset(String id) throws IOException;
- ChangesetPagingResult getChangesets(LogCommandRequest request) throws IOException, RevisionNotFoundException;
+ ChangesetPagingResult getChangesets(LogCommandRequest request) throws IOException;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java
index 9356bb212a..92cd41662b 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/LogCommandRequest.java
@@ -84,7 +84,8 @@ public final class LogCommandRequest implements Serializable, Resetable
&& Objects.equal(pagingStart, other.pagingStart)
&& Objects.equal(pagingLimit, other.pagingLimit)
&& Objects.equal(path, other.path)
- && Objects.equal(branch, other.branch);
+ && Objects.equal(branch, other.branch)
+ && Objects.equal(ancestorChangeset, other.ancestorChangeset);
//J+
}
@@ -98,7 +99,7 @@ public final class LogCommandRequest implements Serializable, Resetable
public int hashCode()
{
return Objects.hashCode(startChangeset, endChangeset, pagingStart,
- pagingLimit, path, branch);
+ pagingLimit, path, branch, ancestorChangeset);
}
/**
@@ -114,6 +115,7 @@ public final class LogCommandRequest implements Serializable, Resetable
pagingLimit = 20;
path = null;
branch = null;
+ ancestorChangeset = null;
}
/**
@@ -133,6 +135,7 @@ public final class LogCommandRequest implements Serializable, Resetable
.add("pagingLimit", pagingLimit)
.add("path", path)
.add("branch", branch)
+ .add("ancestorChangeset", ancestorChangeset)
.toString();
//J+
}
@@ -205,6 +208,10 @@ public final class LogCommandRequest implements Serializable, Resetable
this.startChangeset = startChangeset;
}
+ public void setAncestorChangeset(String ancestorChangeset) {
+ this.ancestorChangeset = ancestorChangeset;
+ }
+
//~--- get methods ----------------------------------------------------------
/**
@@ -284,6 +291,10 @@ public final class LogCommandRequest implements Serializable, Resetable
return pagingLimit < 0;
}
+ public String getAncestorChangeset() {
+ return ancestorChangeset;
+ }
+
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -303,4 +314,6 @@ public final class LogCommandRequest implements Serializable, Resetable
/** Field description */
private String startChangeset;
+
+ private String ancestorChangeset;
}
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
new file mode 100644
index 0000000000..0a3680f6b3
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java
@@ -0,0 +1,10 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.repository.api.MergeCommandResult;
+import sonia.scm.repository.api.MergeDryRunCommandResult;
+
+public interface MergeCommand {
+ MergeCommandResult merge(MergeCommandRequest request);
+
+ MergeDryRunCommandResult dryRun(MergeCommandRequest request);
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java
new file mode 100644
index 0000000000..baf03a0aef
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java
@@ -0,0 +1,93 @@
+package sonia.scm.repository.spi;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import sonia.scm.Validateable;
+import sonia.scm.repository.Person;
+import sonia.scm.util.Util;
+
+import java.io.Serializable;
+
+public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable {
+
+ private static final long serialVersionUID = -2650236557922431528L;
+
+ private String branchToMerge;
+ private String targetBranch;
+ private Person author;
+ private String messageTemplate;
+
+ public String getBranchToMerge() {
+ return branchToMerge;
+ }
+
+ public void setBranchToMerge(String branchToMerge) {
+ this.branchToMerge = branchToMerge;
+ }
+
+ public String getTargetBranch() {
+ return targetBranch;
+ }
+
+ public void setTargetBranch(String targetBranch) {
+ this.targetBranch = targetBranch;
+ }
+
+ public Person getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(Person author) {
+ this.author = author;
+ }
+
+ public String getMessageTemplate() {
+ return messageTemplate;
+ }
+
+ public void setMessageTemplate(String messageTemplate) {
+ this.messageTemplate = messageTemplate;
+ }
+
+ public boolean isValid() {
+ return !Strings.isNullOrEmpty(getBranchToMerge())
+ && !Strings.isNullOrEmpty(getTargetBranch());
+ }
+
+ public void reset() {
+ this.setBranchToMerge(null);
+ this.setTargetBranch(null);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final MergeCommandRequest other = (MergeCommandRequest) obj;
+
+ return Objects.equal(branchToMerge, other.branchToMerge)
+ && Objects.equal(targetBranch, other.targetBranch)
+ && Objects.equal(author, other.author);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(branchToMerge, targetBranch, author);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("branchToMerge", branchToMerge)
+ .add("targetBranch", targetBranch)
+ .add("author", author)
+ .toString();
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java
index e9b40e8a17..322468f827 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java
@@ -32,7 +32,6 @@
package sonia.scm.repository.spi;
import sonia.scm.repository.Modifications;
-import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
@@ -46,8 +45,8 @@ import java.io.IOException;
*/
public interface ModificationsCommand {
- Modifications getModifications(String revision) throws IOException, RevisionNotFoundException;
+ Modifications getModifications(String revision) throws IOException;
- Modifications getModifications(ModificationsCommandRequest request) throws IOException, RevisionNotFoundException;
+ Modifications getModifications(ModificationsCommandRequest request) throws IOException;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
index c66c56c0f1..77201d1a72 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
@@ -251,4 +251,12 @@ public abstract class RepositoryServiceProvider implements Closeable
{
throw new CommandNotSupportedException(Command.UNBUNDLE);
}
+
+ /**
+ * @since 2.0
+ */
+ public MergeCommand getMergeCommand()
+ {
+ throw new CommandNotSupportedException(Command.MERGE);
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java
index fda5e69323..caa35e0b88 100644
--- a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java
+++ b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java
@@ -1,11 +1,19 @@
package sonia.scm.user;
-public class ChangePasswordNotAllowedException extends RuntimeException {
+import sonia.scm.ContextEntry;
+import sonia.scm.ExceptionWithContext;
+public class ChangePasswordNotAllowedException extends ExceptionWithContext {
+
+ private static final String CODE = "9BR7qpDAe1";
public static final String WRONG_USER_TYPE = "User of type %s are not allowed to change password";
- public ChangePasswordNotAllowedException(String type) {
- super(String.format(WRONG_USER_TYPE, type));
+ public ChangePasswordNotAllowedException(ContextEntry.ContextBuilder context, String type) {
+ super(context.build(), String.format(WRONG_USER_TYPE, type));
}
+ @Override
+ public String getCode() {
+ return CODE;
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java
index 870430a1bb..93a6a7c1d1 100644
--- a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java
+++ b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java
@@ -1,8 +1,18 @@
package sonia.scm.user;
-public class InvalidPasswordException extends RuntimeException {
+import sonia.scm.ContextEntry;
+import sonia.scm.ExceptionWithContext;
- public InvalidPasswordException() {
- super("The given Password does not match with the stored one.");
+public class InvalidPasswordException extends ExceptionWithContext {
+
+ private static final String CODE = "8YR7aawFW1";
+
+ public InvalidPasswordException(ContextEntry.ContextBuilder context) {
+ super(context.build(), "The given old password does not match with the stored one.");
+ }
+
+ @Override
+ public String getCode() {
+ return CODE;
}
}
diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
index 7b6d5cb039..e2a2218d34 100644
--- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
+++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
@@ -44,6 +44,7 @@ public class VndMediaType {
public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX;
+ public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX;
private VndMediaType() {
}
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
index b88dda8a97..ffe6ecc787 100644
--- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
+++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
@@ -301,7 +301,7 @@ public class AuthenticationFilter extends HttpFilter
}
}
- chain.doFilter(new SecurityHttpServletRequestWrapper(request, username),
+ chain.doFilter(new PropagatePrincipleServletRequestWrapper(request, username),
response);
}
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java b/scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java
similarity index 78%
rename from scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java
rename to scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java
index 2cd78ce807..2b40b0e73f 100644
--- a/scm-core/src/main/java/sonia/scm/web/filter/SecurityHttpServletRequestWrapper.java
+++ b/scm-core/src/main/java/sonia/scm/web/filter/PropagatePrincipleServletRequestWrapper.java
@@ -38,37 +38,17 @@ package sonia.scm.web.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
-/**
- *
- * @author Sebastian Sdorra
- */
-public class SecurityHttpServletRequestWrapper extends HttpServletRequestWrapper
-{
+public class PropagatePrincipleServletRequestWrapper extends HttpServletRequestWrapper {
- /**
- * Constructs ...
- *
- *
- * @param request
- * @param principal
- */
- public SecurityHttpServletRequestWrapper(HttpServletRequest request,
- String principal)
- {
+ private final String principal;
+
+ public PropagatePrincipleServletRequestWrapper(HttpServletRequest request, String principal) {
super(request);
this.principal = principal;
}
- //~--- get methods ----------------------------------------------------------
-
@Override
- public String getRemoteUser()
- {
+ public String getRemoteUser() {
return principal;
}
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private final String principal;
}
diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java
index 93b0752766..50bde53ae1 100644
--- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java
+++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java
@@ -43,7 +43,6 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.AlreadyExistsException;
-import sonia.scm.NotFoundException;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupNames;
@@ -132,7 +131,7 @@ public class SyncingRealmHelperTest {
* @throws IOException
*/
@Test
- public void testStoreGroupCreate() throws AlreadyExistsException {
+ public void testStoreGroupCreate() {
Group group = new Group("unit-test", "heartOfGold");
helper.store(group);
@@ -143,7 +142,7 @@ public class SyncingRealmHelperTest {
* Tests {@link SyncingRealmHelper#store(Group)}.
*/
@Test(expected = IllegalStateException.class)
- public void testStoreGroupFailure() throws AlreadyExistsException {
+ public void testStoreGroupFailure() {
Group group = new Group("unit-test", "heartOfGold");
doThrow(AlreadyExistsException.class).when(groupManager).create(group);
@@ -169,7 +168,7 @@ public class SyncingRealmHelperTest {
* @throws IOException
*/
@Test
- public void testStoreUserCreate() throws AlreadyExistsException {
+ public void testStoreUserCreate() {
User user = new User("tricia");
helper.store(user);
@@ -180,7 +179,7 @@ public class SyncingRealmHelperTest {
* Tests {@link SyncingRealmHelper#store(User)} with a thrown {@link AlreadyExistsException}.
*/
@Test(expected = IllegalStateException.class)
- public void testStoreUserFailure() throws AlreadyExistsException {
+ public void testStoreUserFailure() {
User user = new User("tricia");
doThrow(AlreadyExistsException.class).when(userManager).create(user);
diff --git a/scm-it/pom.xml b/scm-it/pom.xml
index 251fe4d957..3f518dd9fe 100644
--- a/scm-it/pom.xml
+++ b/scm-it/pom.xml
@@ -80,13 +80,6 @@
test
-
- javax
- javaee-api
- 7.0
- test
-
-
org.glassfish
javax.json
diff --git a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java
new file mode 100644
index 0000000000..0a329b8e1e
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java
@@ -0,0 +1,26 @@
+package sonia.scm.it;
+
+import io.restassured.RestAssured;
+import org.junit.Test;
+import sonia.scm.it.utils.RestUtil;
+import sonia.scm.it.utils.ScmRequests;
+
+import static org.junit.Assert.assertEquals;
+
+public class AnonymousAccessITCase {
+
+ @Test
+ public void shouldAccessIndexResourceWithoutAuthentication() {
+ ScmRequests.start()
+ .requestIndexResource()
+ .assertStatusCode(200);
+ }
+
+ @Test
+ public void shouldRejectUserResourceWithoutAuthentication() {
+ assertEquals(401, RestAssured.given()
+ .when()
+ .get(RestUtil.REST_BASE_URL.resolve("users/"))
+ .statusCode());
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/DiffITCase.java b/scm-it/src/test/java/sonia/scm/it/DiffITCase.java
new file mode 100644
index 0000000000..acd2816422
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/DiffITCase.java
@@ -0,0 +1,265 @@
+package sonia.scm.it;
+
+import org.apache.http.HttpStatus;
+import org.assertj.core.util.Lists;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import sonia.scm.it.utils.RepositoryUtil;
+import sonia.scm.it.utils.ScmRequests;
+import sonia.scm.it.utils.TestData;
+import sonia.scm.repository.Changeset;
+import sonia.scm.repository.client.api.RepositoryClient;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
+import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
+
+public class DiffITCase {
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+ private RepositoryClient svnRepositoryClient;
+ private RepositoryClient gitRepositoryClient;
+ private RepositoryClient hgRepositoryClient;
+ private ScmRequests.RepositoryResponse svnRepositoryResponse;
+ private ScmRequests.RepositoryResponse hgRepositoryResponse;
+ private ScmRequests.RepositoryResponse gitRepositoryResponse;
+ private File svnFolder;
+ private File gitFolder;
+ private File hgFolder;
+
+ @Before
+ public void init() throws IOException {
+ TestData.createDefault();
+ String namespace = ADMIN_USERNAME;
+ String repo = TestData.getDefaultRepoName("svn");
+ svnRepositoryResponse =
+ ScmRequests.start()
+ .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
+ .requestRepository(namespace, repo)
+ .assertStatusCode(HttpStatus.SC_OK);
+ svnFolder = tempFolder.newFolder("svn");
+ svnRepositoryClient = RepositoryUtil.createRepositoryClient("svn", svnFolder);
+
+ repo = TestData.getDefaultRepoName("git");
+ gitRepositoryResponse =
+ ScmRequests.start()
+ .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
+ .requestRepository(namespace, repo)
+ .assertStatusCode(HttpStatus.SC_OK);
+ gitFolder = tempFolder.newFolder("git");
+ gitRepositoryClient = RepositoryUtil.createRepositoryClient("git", gitFolder);
+
+ repo = TestData.getDefaultRepoName("hg");
+ hgRepositoryResponse =
+ ScmRequests.start()
+ .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
+ .requestRepository(namespace, repo)
+ .assertStatusCode(HttpStatus.SC_OK);
+ hgFolder = tempFolder.newFolder("hg");
+ hgRepositoryClient = RepositoryUtil.createRepositoryClient("hg", hgFolder);
+ }
+
+ @Test
+ public void shouldFindDiffsInGitFormat() throws IOException {
+ String svnDiff = getDiff(RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), svnRepositoryResponse);
+ String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse);
+ String hgDiff = getDiff(RepositoryUtil.createAndCommitFile(hgRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), hgRepositoryResponse);
+
+ assertThat(Lists.newArrayList(svnDiff, gitDiff, hgDiff))
+ .allSatisfy(diff -> assertThat(diff)
+ .contains("diff --git "));
+ }
+
+ @Test
+ public void svnAddFileDiffShouldBeConvertedToGitDiff() throws IOException {
+ String svnDiff = getDiff(RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), svnRepositoryResponse);
+ String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse);
+
+ String expected = getGitDiffWithoutIndexLine(gitDiff);
+ assertThat(svnDiff)
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void svnDeleteFileDiffShouldBeConvertedToGitDiff() throws IOException {
+ RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
+ RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
+
+ String svnDiff = getDiff(RepositoryUtil.removeAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt"), svnRepositoryResponse);
+ String gitDiff = getDiff(RepositoryUtil.removeAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt"), gitRepositoryResponse);
+
+ String expected = getGitDiffWithoutIndexLine(gitDiff);
+ assertThat(svnDiff)
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void svnUpdateFileDiffShouldBeConvertedToGitDiff() throws IOException {
+ RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
+ RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
+
+ String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), svnRepositoryResponse);
+ String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), gitRepositoryResponse);
+
+ String expected = getGitDiffWithoutIndexLine(gitDiff);
+ assertThat(svnDiff)
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void svnMultipleChangesDiffShouldBeConvertedToGitDiff() throws IOException {
+ String svnDiff = getDiff(applyMultipleChanges(svnRepositoryClient, "fileToBeDeleted.txt", "fileToBeUpdated.txt", "addedFile.txt"), svnRepositoryResponse);
+ String gitDiff = getDiff(applyMultipleChanges(gitRepositoryClient, "fileToBeDeleted.txt", "fileToBeUpdated.txt", "addedFile.txt"), gitRepositoryResponse);
+
+ String endOfDiffPart = "\\ No newline at end of file\n";
+ String[] gitDiffs = gitDiff.split(endOfDiffPart);
+ List expected = Arrays.stream(gitDiffs)
+ .map(this::getGitDiffWithoutIndexLine)
+ .collect(Collectors.toList());
+ assertThat(svnDiff.split(endOfDiffPart))
+ .containsExactlyInAnyOrderElementsOf(expected);
+ }
+
+ @Test
+ public void svnMultipleSubFolderChangesDiffShouldBeConvertedToGitDiff() throws IOException {
+ String svnDiff = getDiff(applyMultipleChanges(svnRepositoryClient, "a/b/fileToBeDeleted.txt", "a/c/fileToBeUpdated.txt", "a/d/addedFile.txt"), svnRepositoryResponse);
+ String gitDiff = getDiff(applyMultipleChanges(gitRepositoryClient, "a/b/fileToBeDeleted.txt", "a/c/fileToBeUpdated.txt", "a/d/addedFile.txt"), gitRepositoryResponse);
+
+ String endOfDiffPart = "\\ No newline at end of file\n";
+ String[] gitDiffs = gitDiff.split(endOfDiffPart);
+ List expected = Arrays.stream(gitDiffs)
+ .map(this::getGitDiffWithoutIndexLine)
+ .collect(Collectors.toList());
+ assertThat(svnDiff.split(endOfDiffPart))
+ .containsExactlyInAnyOrderElementsOf(expected);
+ }
+
+ @Test
+ public void svnLargeChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException {
+ String fileName = "SvnDiffGenerator_forTest";
+ RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, "");
+ RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, "");
+
+ String fileContent = getFileContent("/diff/largefile/original/SvnDiffGenerator_forTest");
+ String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
+ String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
+ assertThat(svnDiff)
+ .isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
+
+ fileContent = getFileContent("/diff/largefile/modified/v1/SvnDiffGenerator_forTest");
+ svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
+ gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
+ assertThat(svnDiff)
+ .isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
+
+ fileContent = getFileContent("/diff/largefile/modified/v2/SvnDiffGenerator_forTest");
+ svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
+ gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
+ assertThat(svnDiff)
+ .isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
+
+ }
+
+ /**
+ * FIXME: the binary Git Diff output is not GIT conform
+ */
+ @Test
+ @Ignore
+ @SuppressWarnings("squid:S1607")
+ public void svnBinaryChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException {
+ String fileName = "binary";
+ File file = new File(svnRepositoryClient.getWorkingCopy(), fileName);
+ Files.copy(Paths.get(getClass().getResource("/diff/binaryfile/echo").toURI()), Paths.get(file.toURI()));
+ Changeset commit = RepositoryUtil.addFileAndCommit(svnRepositoryClient, fileName, ADMIN_USERNAME, "");
+
+ file = new File(gitRepositoryClient.getWorkingCopy(), fileName);
+ Files.copy(Paths.get(getClass().getResource("/diff/binaryfile/echo").toURI()), Paths.get(file.toURI()));
+
+ Changeset commit1 = RepositoryUtil.addFileAndCommit(gitRepositoryClient, fileName, ADMIN_USERNAME, "");
+ String svnDiff = getDiff(commit, svnRepositoryResponse);
+ String gitDiff = getDiff(commit1, gitRepositoryResponse);
+ assertThat(svnDiff)
+ .isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
+
+ }
+
+ @Test
+ public void svnRenameChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException {
+ String fileName = "a.txt";
+ RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, "content of a");
+ RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, "content of a");
+
+ String newFileName = "renamed_a.txt";
+ File file = new File(svnRepositoryClient.getWorkingCopy(), fileName);
+ file.renameTo(new File(svnRepositoryClient.getWorkingCopy(), newFileName));
+
+ String svnDiff = getDiff(RepositoryUtil.addFileAndCommit(svnRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), svnRepositoryResponse);
+
+ file = new File(gitRepositoryClient.getWorkingCopy(), fileName);
+ file.renameTo(new File(gitRepositoryClient.getWorkingCopy(), newFileName));
+ String gitDiff = getDiff(RepositoryUtil.addFileAndCommit(gitRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), gitRepositoryResponse);
+
+ String expected = getGitDiffWithoutIndexLine(gitDiff);
+ assertThat(svnDiff)
+ .isEqualTo(expected);
+ }
+
+ public String getFileContent(String name) throws URISyntaxException, IOException {
+ Path path;
+ path = Paths.get(getClass().getResource(name).toURI());
+ Stream lines = Files.lines(path);
+ String data = lines.collect(Collectors.joining("\n"));
+ lines.close();
+ return data;
+ }
+
+ /**
+ * The index line is not provided from the svn git formatter and it is not needed in the ui diff view
+ * for more details about the git diff format: https://git-scm.com/docs/git-diff
+ *
+ * @param gitDiff
+ * @return diff without the index line
+ */
+ private String getGitDiffWithoutIndexLine(String gitDiff) {
+ return gitDiff.replaceAll(".*(index.*\n)", "");
+ }
+
+ private String getDiff(Changeset svnChangeset, ScmRequests.RepositoryResponse svnRepositoryResponse) {
+ return svnRepositoryResponse.requestChangesets()
+ .requestDiffInGitFormat(svnChangeset.getId())
+ .getResponse()
+ .body()
+ .asString();
+ }
+
+ private Changeset applyMultipleChanges(RepositoryClient repositoryClient, String fileToBeDeleted, final String fileToBeUpdated, final String addedFile) throws IOException {
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileToBeDeleted, "file to be deleted");
+ RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileToBeUpdated, "file to be updated");
+ Map addedFiles = new HashMap() {{
+ put(addedFile, "content");
+ }};
+ Map modifiedFiles = new HashMap() {{
+ put(fileToBeUpdated, "the updated content");
+ }};
+ ArrayList removedFiles = Lists.newArrayList(fileToBeDeleted);
+ return RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java
index 8785f1d8ce..aa91e67022 100644
--- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java
+++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java
@@ -32,6 +32,7 @@
package sonia.scm.it;
import org.apache.http.HttpStatus;
+import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -49,8 +50,10 @@ import sonia.scm.web.VndMediaType;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
@@ -72,7 +75,7 @@ public class PermissionsITCase {
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final String repositoryType;
- private int createdPermissions;
+ private Collection createdPermissions;
public PermissionsITCase(String repositoryType) {
@@ -94,7 +97,7 @@ public class PermissionsITCase {
TestData.createNotAdminUser(USER_OWNER, USER_PASS);
TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType);
TestData.createNotAdminUser(USER_OTHER, USER_PASS);
- createdPermissions = 3;
+ createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER);
}
@Test
@@ -131,8 +134,8 @@ public class PermissionsITCase {
@Test
public void ownerShouldSeePermissions() {
- List userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType);
- assertEquals(userPermissions.size(), createdPermissions);
+ List userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType);
+ Assertions.assertThat(userPermissions).extracting(e -> e.get("name")).containsAll(createdPermissions);
}
@Test
diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
index 66ebc57c90..83baa89463 100644
--- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
+++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
@@ -1,5 +1,6 @@
package sonia.scm.it;
+import groovy.util.logging.Slf4j;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.apache.http.HttpStatus;
@@ -37,6 +38,7 @@ import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
@RunWith(Parameterized.class)
+@Slf4j
public class RepositoryAccessITCase {
@Rule
@@ -63,9 +65,9 @@ public class RepositoryAccessITCase {
String repo = TestData.getDefaultRepoName(repositoryType);
repositoryResponse =
ScmRequests.start()
- .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
- .requestRepository(namespace, repo)
- .assertStatusCode(HttpStatus.SC_OK);
+ .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
+ .requestRepository(namespace, repo)
+ .assertStatusCode(HttpStatus.SC_OK);
}
@Test
@@ -175,6 +177,7 @@ public class RepositoryAccessITCase {
}
@Test
+ @SuppressWarnings("squid:S2925")
public void shouldReadContent() throws IOException, InterruptedException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a");
@@ -262,40 +265,6 @@ public class RepositoryAccessITCase {
assertThat(changesets).size().isBetween(2, 3); // svn has an implicit root revision '0' that is extra to the two commits
}
- @Test
- public void shouldFindDiffs() throws IOException {
- RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
-
- RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a");
- RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "b.txt", "b");
-
- String changesetsUrl = given()
- .when()
- .get(TestData.getDefaultRepositoryUrl(repositoryType))
- .then()
- .statusCode(HttpStatus.SC_OK)
- .extract()
- .path("_links.changesets.href");
-
- String diffUrl = given()
- .when()
- .get(changesetsUrl)
- .then()
- .statusCode(HttpStatus.SC_OK)
- .extract()
- .path("_embedded.changesets[0]._links.diff.href");
-
- given()
- .when()
- .get(diffUrl)
- .then()
- .statusCode(HttpStatus.SC_OK)
- .extract()
- .body()
- .asString()
- .contains("diff");
-
- }
@Test
@SuppressWarnings("unchecked")
@@ -393,12 +362,10 @@ public class RepositoryAccessITCase {
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "b.txt", "b");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "c.txt", "c");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "d.txt", "d");
- Map addedFiles = new HashMap()
- {{
+ Map addedFiles = new HashMap() {{
put("a.txt", "bla bla");
}};
- Map modifiedFiles = new HashMap()
- {{
+ Map modifiedFiles = new HashMap() {{
put("b.txt", "new content");
}};
ArrayList removedFiles = Lists.newArrayList("c.txt", "d.txt");
@@ -414,7 +381,7 @@ public class RepositoryAccessITCase {
.assertAdded(a -> assertThat(a)
.hasSize(1)
.containsExactly("a.txt"))
- .assertModified(m-> assertThat(m)
+ .assertModified(m -> assertThat(m)
.hasSize(1)
.containsExactly("b.txt"))
.assertRemoved(r -> assertThat(r)
diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
index 11db0200f1..427d98f245 100644
--- a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
+++ b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
@@ -80,6 +80,11 @@ public class RepositoryUtil {
return file;
}
+ public static Changeset updateAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
+ writeAndAddFile(repositoryClient, fileName, content);
+ return commit(repositoryClient, username, "updated " + fileName);
+ }
+
public static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
return commit(repositoryClient, username, "removed " + fileName);
@@ -102,11 +107,21 @@ public class RepositoryUtil {
} else {
path = thisName;
}
- repositoryClient.getAddCommand().add(path);
+ addFile(repositoryClient, path);
return path;
}
- static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException {
+ public static Changeset addFileAndCommit(RepositoryClient repositoryClient, String path, String username, String message) throws IOException {
+ repositoryClient.getAddCommand().add(path);
+ return commit(repositoryClient, username, message);
+ }
+
+
+ public static void addFile(RepositoryClient repositoryClient, String path) throws IOException {
+ repositoryClient.getAddCommand().add(path);
+ }
+
+ public static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException {
LOG.info("user: {} try to commit with message: {}", username, message);
Changeset changeset = repositoryClient.getCommitCommand().commit(new Person(username, username + "@scm-manager.org"), message);
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {
diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
index 14caa57beb..bde3892773 100644
--- a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
+++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
@@ -38,6 +38,10 @@ public class ScmRequests {
return new ScmRequests();
}
+ public IndexResponse requestIndexResource() {
+ return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString()));
+ }
+
public IndexResponse requestIndexResource(String username, String password) {
setUsername(username);
setPassword(password);
@@ -234,8 +238,8 @@ public class ScmRequests {
return this;
}
- public DiffResponse requestDiff(String revision) {
- return new DiffResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"), this);
+ public DiffResponse requestDiffInGitFormat(String revision) {
+ return new DiffResponse<>(applyGETRequestFromLinkWithParams(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href", "?format=GIT"), this);
}
public ModificationsResponse requestModifications(String revision) {
@@ -362,6 +366,10 @@ public class ScmRequests {
this.previousResponse = previousResponse;
}
+ public Response getResponse(){
+ return response;
+ }
+
public PREV returnToPrevious() {
return previousResponse;
}
diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java
index a164fc6649..48605437c6 100644
--- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java
+++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java
@@ -99,10 +99,10 @@ public class TestData {
;
}
- public static List getUserPermissions(String username, String password, String repositoryType) {
+ public static List getUserPermissions(String username, String password, String repositoryType) {
return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK)
.extract()
- .body().jsonPath().getList("_embedded.permissions");
+ .body().jsonPath().getList("_embedded.permissions");
}
public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) {
diff --git a/scm-it/src/test/resources/diff/binaryfile/echo b/scm-it/src/test/resources/diff/binaryfile/echo
new file mode 100755
index 0000000000..11bc2152e4
Binary files /dev/null and b/scm-it/src/test/resources/diff/binaryfile/echo differ
diff --git a/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest
new file mode 100644
index 0000000000..f714a955c7
--- /dev/null
+++ b/scm-it/src/test/resources/diff/largefile/modified/v1/SvnDiffGenerator_forTest
@@ -0,0 +1,1230 @@
+package sonia.scm.repository.spi;
+
+import de.regnis.q.sequence.line.diff.QDiffGenerator;
+import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory;
+import de.regnis.q.sequence.line.diff.QDiffManager;
+import de.regnis.q.sequence.line.diff.QDiffUniGenerator;
+import org.tmatesoft.svn.core.SVNErrorCode;
+import org.tmatesoft.svn.core.SVNErrorMessage;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.SVNMergeRangeList;
+import org.tmatesoft.svn.core.SVNProperties;
+import org.tmatesoft.svn.core.SVNProperty;
+import org.tmatesoft.svn.core.SVNPropertyValue;
+import org.tmatesoft.svn.core.internal.util.SVNHashMap;
+import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
+import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
+import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
+import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback;
+import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
+import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
+import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator;
+import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback;
+import org.tmatesoft.svn.core.wc.ISVNOptions;
+import org.tmatesoft.svn.core.wc.SVNDiffOptions;
+import org.tmatesoft.svn.core.wc2.SvnTarget;
+import org.tmatesoft.svn.util.SVNLogType;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.RandomAccessFile;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+public class SCMSvnDiffGenerator implements ISvnDiffGenerator {
+
+ protected static final String WC_REVISION_LABEL = "(working copy)";
+ protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________";
+ protected static final String HEADER_SEPARATOR = "===================================================================";
+ protected static final String HEADER_ENCODING = "UTF-8";
+
+ private SvnTarget originalTarget1;
+ private SvnTarget originalTarget2;
+ private SvnTarget baseTarget;
+ private SvnTarget relativeToTarget;
+ private SvnTarget repositoryRoot;
+ private String encoding;
+ private byte[] eol;
+ private boolean useGitFormat;
+ private boolean forcedBinaryDiff;
+
+ private boolean diffDeleted;
+ private boolean diffAdded;
+ private List rawDiffOptions;
+ private boolean forceEmpty;
+
+ private Set visitedPaths;
+ private String externalDiffCommand;
+ private SVNDiffOptions diffOptions;
+ private boolean fallbackToAbsolutePath;
+ private ISVNOptions options;
+ private boolean propertiesOnly;
+ private boolean ignoreProperties;
+
+ private String getDisplayPath(SvnTarget target) {
+ String relativePath;
+ if (baseTarget == null) {
+ relativePath = null;
+ } else {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = baseTarget.getPathOrUrlDecodedString();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ }
+
+ return relativePath != null ? relativePath : target.getPathOrUrlString();
+ }
+
+ private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) {
+ String relativePath;
+ if (repositoryRoot == null) {
+ relativePath = null;
+ } else {
+ if (repositoryRoot.isFile() == target.isFile()) {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = repositoryRoot.getPathOrUrlDecodedString();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ } else {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = new File("").getAbsolutePath();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ }
+ }
+
+ return relativePath != null ? relativePath : target.getPathOrUrlString();
+ }
+
+ private String getRelativePath(String targetString, String baseTargetString) {
+ if (targetString != null) {
+ targetString = targetString.replace(File.separatorChar, '/');
+ }
+ if (baseTargetString != null) {
+ baseTargetString = baseTargetString.replace(File.separatorChar, '/');
+ }
+
+ final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString);
+ if (pathAsChild != null) {
+ return pathAsChild;
+ }
+ if (targetString.equals(baseTargetString)) {
+ return "";
+ }
+ return null;
+ }
+
+ private String getChildPath(String path, String relativeToPath) {
+ if (relativeToTarget == null) {
+ return null;
+ }
+
+ String relativePath = getRelativePath(path, relativeToPath);
+ if (relativePath == null) {
+ return path;
+ }
+
+ if (relativePath.length() > 0) {
+ return relativePath;
+ }
+
+ if (relativeToPath.equals(path)) {
+ return ".";
+ }
+
+ return null;
+ }
+
+ public SCMSvnDiffGenerator() {
+ this.originalTarget1 = null;
+ this.originalTarget2 = null;
+ this.visitedPaths = new HashSet();
+ this.diffDeleted = true;
+ this.diffAdded = true;
+ }
+
+ public void setBaseTarget(SvnTarget baseTarget) {
+ this.baseTarget = baseTarget;
+ }
+
+ public void setUseGitFormat(boolean useGitFormat) {
+ this.useGitFormat = useGitFormat;
+ }
+
+ public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) {
+ this.originalTarget1 = originalTarget1;
+ this.originalTarget2 = originalTarget2;
+ }
+
+ public void setRelativeToTarget(SvnTarget relativeToTarget) {
+ this.relativeToTarget = relativeToTarget;
+ }
+
+ public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) {
+ //anchors are not used
+ }
+
+ public void setRepositoryRoot(SvnTarget repositoryRoot) {
+ this.repositoryRoot = repositoryRoot;
+ }
+
+ public void setForceEmpty(boolean forceEmpty) {
+ this.forceEmpty = forceEmpty;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public String getGlobalEncoding() {
+ ISVNOptions options = getOptions();
+
+ if (options != null && options instanceof DefaultSVNOptions) {
+ DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options;
+ return defaultOptions.getGlobalCharset();
+ }
+ return null;
+ }
+
+ public void setEOL(byte[] eol) {
+ this.eol = eol;
+ }
+
+ public byte[] getEOL() {
+ return eol;
+ }
+
+ public boolean isForcedBinaryDiff() {
+ return forcedBinaryDiff;
+ }
+
+ public void setForcedBinaryDiff(boolean forcedBinaryDiff) {
+ this.forcedBinaryDiff = forcedBinaryDiff;
+ }
+
+ public boolean isPropertiesOnly() {
+ return propertiesOnly;
+ }
+
+ public void setPropertiesOnly(boolean propertiesOnly) {
+ this.propertiesOnly = propertiesOnly;
+ }
+
+ public boolean isIgnoreProperties() {
+ return ignoreProperties;
+ }
+
+ public void setIgnoreProperties(boolean ignoreProperties) {
+ this.ignoreProperties = ignoreProperties;
+ }
+
+ public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException {
+ }
+
+ public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException {
+ }
+
+ public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException {
+ if (isIgnoreProperties()) {
+ return;
+ }
+ if (dirWasAdded && !isDiffAdded()) {
+ return;
+ }
+ ensureEncodingAndEOLSet();
+ String displayPath = getDisplayPath(target);
+
+ String targetString1 = originalTarget1.getPathOrUrlDecodedString();
+ String targetString2 = originalTarget2.getPathOrUrlDecodedString();
+
+ if (displayPath == null || displayPath.length() == 0) {
+ displayPath = ".";
+ }
+
+ if (useGitFormat) {
+ targetString1 = adjustRelativeToReposRoot(targetString1);
+ targetString2 = adjustRelativeToReposRoot(targetString2);
+ }
+
+ String newTargetString = displayPath;
+ String newTargetString1 = targetString1;
+ String newTargetString2 = targetString2;
+
+ String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2);
+ int commonLength = commonAncestor == null ? 0 : commonAncestor.length();
+
+ newTargetString1 = newTargetString1.substring(commonLength);
+ newTargetString2 = newTargetString2.substring(commonLength);
+
+ newTargetString1 = computeLabel(newTargetString, newTargetString1);
+ newTargetString2 = computeLabel(newTargetString, newTargetString2);
+
+ if (relativeToTarget != null) {
+ String relativeToPath = relativeToTarget.getPathOrUrlDecodedString();
+ String absolutePath = target.getPathOrUrlDecodedString();
+
+ String childPath = getChildPath(absolutePath, relativeToPath);
+ if (childPath == null) {
+ throwBadRelativePathException(absolutePath, relativeToPath);
+ }
+ String childPath1 = getChildPath(newTargetString1, relativeToPath);
+ if (childPath1 == null) {
+ throwBadRelativePathException(newTargetString1, relativeToPath);
+ }
+ String childPath2 = getChildPath(newTargetString2, relativeToPath);
+ if (childPath2 == null) {
+ throwBadRelativePathException(newTargetString2, relativeToPath);
+ }
+
+ displayPath = childPath;
+ newTargetString1 = childPath1;
+ newTargetString2 = childPath2;
+ }
+
+ boolean showDiffHeader = !visitedPaths.contains(displayPath);
+ if (showDiffHeader) {
+ String label1 = getLabel(newTargetString1, revision1);
+ String label2 = getLabel(newTargetString2, revision2);
+
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified);
+ visitedPaths.add(displayPath);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+// if (useGitFormat) {
+// String copyFromPath = null;
+// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified;
+// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1);
+// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2);
+// displayGitDiffHeader(outputStream, operationKind,
+// getRelativeToRootPath(target, originalTarget1),
+// getRelativeToRootPath(target, originalTarget2),
+// copyFromPath);
+// }
+
+ if (useGitFormat) {
+ displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null);
+ } else {
+ displayHeaderFields(outputStream, label1, label2);
+ }
+ }
+
+ displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream);
+
+ displayPropDiffValues(outputStream, propChanges, originalProps);
+ }
+
+ private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException {
+ SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''",
+ displayPath, relativeToPath);
+ SVNErrorManager.error(errorMessage, SVNLogType.CLIENT);
+ }
+
+ private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1);
+ String path2 = getRelativeToRootPath(target, originalTarget2);
+
+ try {
+ displayString(outputStream, "--- ");
+ displayFirstGitLabelPath(outputStream, path1, revision1, operation);
+ displayEOL(outputStream);
+ displayString(outputStream, "+++ ");
+ displaySecondGitLabelPath(outputStream, path2, revision2, operation);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private String adjustRelativeToReposRoot(String targetString) {
+ if (repositoryRoot != null) {
+ String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString();
+ String relativePath = getRelativePath(targetString, repositoryRootString);
+ return relativePath == null ? "" : relativePath;
+ }
+ return targetString;
+ }
+
+ private String computeLabel(String targetString, String originalTargetString) {
+ if (originalTargetString.length() == 0) {
+ return targetString;
+ } else if (originalTargetString.charAt(0) == '/') {
+ return targetString + "\t(..." + originalTargetString + ")";
+ } else {
+ return targetString + "\t(.../" + originalTargetString + ")";
+ }
+ }
+
+ public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException {
+ if (isPropertiesOnly()) {
+ return;
+ }
+ ensureEncodingAndEOLSet();
+ String displayPath = getDisplayPath(target);
+
+ String targetString1 = originalTarget1.getPathOrUrlDecodedString();
+ String targetString2 = originalTarget2.getPathOrUrlDecodedString();
+
+ if (useGitFormat) {
+ targetString1 = adjustRelativeToReposRoot(targetString1);
+ targetString2 = adjustRelativeToReposRoot(targetString2);
+ }
+
+ String newTargetString = displayPath;
+ String newTargetString1 = targetString1;
+ String newTargetString2 = targetString2;
+
+ String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2);
+ int commonLength = commonAncestor == null ? 0 : commonAncestor.length();
+
+ newTargetString1 = newTargetString1.substring(commonLength);
+ newTargetString2 = newTargetString2.substring(commonLength);
+
+ newTargetString1 = computeLabel(newTargetString, newTargetString1);
+ newTargetString2 = computeLabel(newTargetString, newTargetString2);
+
+ if (relativeToTarget != null) {
+ String relativeToPath = relativeToTarget.getPathOrUrlDecodedString();
+ String absolutePath = target.getPathOrUrlDecodedString();
+
+ String childPath = getChildPath(absolutePath, relativeToPath);
+ if (childPath == null) {
+ throwBadRelativePathException(absolutePath, relativeToPath);
+ }
+ String childPath1 = getChildPath(newTargetString1, relativeToPath);
+ if (childPath1 == null) {
+ throwBadRelativePathException(newTargetString1, relativeToPath);
+ }
+ String childPath2 = getChildPath(newTargetString2, relativeToPath);
+ if (childPath2 == null) {
+ throwBadRelativePathException(newTargetString2, relativeToPath);
+ }
+
+ displayPath = childPath;
+ newTargetString1 = childPath1;
+ newTargetString2 = childPath2;
+ }
+
+ String label1 = getLabel(newTargetString1, revision1);
+ String label2 = getLabel(newTargetString2, revision2);
+
+ boolean leftIsBinary = false;
+ boolean rightIsBinary = false;
+
+ if (mimeType1 != null) {
+ leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1);
+ }
+ if (mimeType2 != null) {
+ rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2);
+ }
+
+ if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) {
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ visitedPaths.add(displayPath);
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+
+ displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary);
+
+ return;
+ }
+
+ final String diffCommand = getExternalDiffCommand();
+ if (diffCommand != null) {
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ visitedPaths.add(displayPath);
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+ runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2);
+ } else {
+ internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2);
+ }
+ }
+
+ private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException {
+ displayCannotDisplayFileMarkedBinary(outputStream);
+
+ if (leftIsBinary && !rightIsBinary) {
+ displayMimeType(outputStream, mimeType1);
+ } else if (!leftIsBinary && rightIsBinary) {
+ displayMimeType(outputStream, mimeType2);
+ } else if (leftIsBinary && rightIsBinary) {
+ if (mimeType1.equals(mimeType2)) {
+ displayMimeType(outputStream, mimeType1);
+ } else {
+ displayMimeTypes(outputStream, mimeType1, mimeType2);
+ }
+ }
+ }
+
+ private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException {
+ String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath);
+ if (file2 == null && !isDiffDeleted()) {
+ try {
+ displayString(outputStream, header);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ visitedPaths.add(displayPath);
+ return;
+ }
+ if (file1 == null && !isDiffAdded()) {
+ try {
+ displayString(outputStream, header);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ visitedPaths.add(displayPath);
+ return;
+ }
+ String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath);
+
+ RandomAccessFile is1 = null;
+ RandomAccessFile is2 = null;
+ try {
+ is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1);
+ is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2);
+
+ QDiffUniGenerator.setup();
+ Map properties = new SVNHashMap();
+
+ properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle()));
+ properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL()));
+ if (getDiffOptions().isIgnoreAllWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE);
+ } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE);
+ }
+
+ final String diffHeader;
+ if (forceEmpty || useGitFormat) {
+ displayString(outputStream, header);
+ diffHeader = headerFields;
+
+ visitedPaths.add(displayPath);
+ } else {
+ diffHeader = header + headerFields;
+ }
+ QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader);
+ EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream);
+ QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator);
+ if (emptyDetectionOutputStream.isSomethingWritten()) {
+ visitedPaths.add(displayPath);
+ }
+ emptyDetectionOutputStream.flush();
+ } catch (IOException e) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage());
+ SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
+ } finally {
+ SVNFileUtil.closeFile(is1);
+ SVNFileUtil.closeFile(is2);
+ }
+ }
+
+ private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ try {
+ if (useGitFormat) {
+ displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath);
+ } else {
+ displayHeaderFields(byteArrayOutputStream, label1, label2);
+ }
+ } catch (SVNException e) {
+ SVNFileUtil.closeFile(byteArrayOutputStream);
+
+ try {
+ byteArrayOutputStream.writeTo(byteArrayOutputStream);
+ } catch (IOException e1) {
+ }
+
+ throw e;
+ }
+
+ try {
+ byteArrayOutputStream.close();
+ return byteArrayOutputStream.toString(HEADER_ENCODING);
+ } catch (IOException e) {
+ return "";
+ }
+ }
+
+ private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ try {
+ boolean stopDisplaying = displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation);
+ if (useGitFormat) {
+ displayGitDiffHeader(byteArrayOutputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ copyFromPath);
+ }
+ } catch (SVNException e) {
+ SVNFileUtil.closeFile(byteArrayOutputStream);
+
+ try {
+ byteArrayOutputStream.writeTo(byteArrayOutputStream);
+ } catch (IOException e1) {
+ }
+
+ throw e;
+ }
+
+ try {
+ byteArrayOutputStream.close();
+ return byteArrayOutputStream.toString(HEADER_ENCODING);
+ } catch (IOException e) {
+ return "";
+ }
+ }
+
+ private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException {
+ final List args = new ArrayList();
+ args.add(diffCommand);
+ if (rawDiffOptions != null) {
+ args.addAll(rawDiffOptions);
+ } else {
+ Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection();
+ args.addAll(svnDiffOptionsCollection);
+ args.add("-u");
+ }
+
+ if (label1 != null) {
+ args.add("-L");
+ args.add(label1);
+ }
+
+ if (label2 != null) {
+ args.add("-L");
+ args.add(label2);
+ }
+
+ boolean tmpFile1 = false;
+ boolean tmpFile2 = false;
+ if (file1 == null) {
+ file1 = SVNFileUtil.createTempFile("svn.", ".tmp");
+ tmpFile1 = true;
+ }
+ if (file2 == null) {
+ file2 = SVNFileUtil.createTempFile("svn.", ".tmp");
+ tmpFile2 = true;
+ }
+
+ String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/');
+ String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/');
+
+ args.add(file1Path);
+ args.add(file2Path);
+ try {
+ final Writer writer = new OutputStreamWriter(outputStream, getEncoding());
+
+ SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true,
+ new ISVNReturnValueCallback() {
+
+ public void handleReturnValue(int returnValue) throws SVNException {
+ if (returnValue != 0 && returnValue != 1) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM,
+ "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)});
+ SVNErrorManager.error(err, SVNLogType.DEFAULT);
+ }
+ }
+
+ public void handleChar(char ch) throws SVNException {
+ try {
+ writer.write(ch);
+ } catch (IOException ioe) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage());
+ SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT);
+ }
+ }
+
+ public boolean isHandleProgramOutput() {
+ return true;
+ }
+ });
+
+ writer.flush();
+ } catch (IOException ioe) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage());
+ SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT);
+ } finally {
+ try {
+ if (tmpFile1) {
+ SVNFileUtil.deleteFile(file1);
+ }
+ if (tmpFile2) {
+ SVNFileUtil.deleteFile(file2);
+ }
+ } catch (SVNException e) {
+ // skip
+ }
+ }
+ }
+
+ private String getExternalDiffCommand() {
+ return externalDiffCommand;
+ }
+
+ private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException {
+ try {
+ displayString(outputStream, SVNProperty.MIME_TYPE);
+ displayString(outputStream, " = ");
+ displayString(outputStream, mimeType);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException {
+ try {
+ displayString(outputStream, SVNProperty.MIME_TYPE);
+ displayString(outputStream, " = (");
+ displayString(outputStream, mimeType1);
+ displayString(outputStream, ", ");
+ displayString(outputStream, mimeType2);
+ displayString(outputStream, ")");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException {
+ try {
+ displayString(outputStream, "Cannot display: file marked as a binary type.");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void ensureEncodingAndEOLSet() {
+ if (getEOL() == null) {
+ setEOL(SVNProperty.EOL_LF_BYTES);
+ }
+ if (getEncoding() == null) {
+ final ISVNOptions options = getOptions();
+ if (options != null && options.getNativeCharset() != null) {
+ setEncoding(options.getNativeCharset());
+ } else {
+ setEncoding("UTF-8");
+ }
+ }
+ }
+
+ private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException {
+ for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) {
+ String name = (String) changedPropNames.next();
+ SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null;
+ SVNPropertyValue newValue = diff.getSVNPropertyValue(name);
+ String headerFormat = null;
+
+ if (originalValue == null) {
+ headerFormat = "Added: ";
+ } else if (newValue == null) {
+ headerFormat = "Deleted: ";
+ } else {
+ headerFormat = "Modified: ";
+ }
+
+ try {
+ displayString(outputStream, (headerFormat + name));
+ displayEOL(outputStream);
+ if (SVNProperty.MERGE_INFO.equals(name)) {
+ displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString());
+ continue;
+ }
+
+ byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding());
+ byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding());
+
+ if (originalValueBytes == null) {
+ originalValueBytes = new byte[0];
+ } else {
+ originalValueBytes = maybeAppendEOL(originalValueBytes);
+ }
+
+ boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 &&
+ (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] ||
+ newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]);
+
+ if (newValueBytes == null) {
+ newValueBytes = new byte[0];
+ } else {
+ newValueBytes = maybeAppendEOL(newValueBytes);
+ }
+
+ QDiffUniGenerator.setup();
+ Map properties = new SVNHashMap();
+
+ properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle()));
+ properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL()));
+ properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##");
+ if (getDiffOptions().isIgnoreAllWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE);
+ } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE);
+ }
+
+ QDiffGenerator generator = new QDiffUniGenerator(properties, "");
+ Writer writer = new OutputStreamWriter(outputStream, getEncoding());
+ QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes),
+ null, writer, generator);
+ writer.flush();
+ if (!newValueHadEol) {
+ displayString(outputStream, "\\ No newline at end of property");
+ displayEOL(outputStream);
+ }
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ }
+
+ private byte[] maybeAppendEOL(byte[] buffer) {
+ if (buffer.length == 0) {
+ return buffer;
+ }
+
+ byte lastByte = buffer[buffer.length - 1];
+ if (lastByte == SVNProperty.EOL_CR_BYTES[0]) {
+ return buffer;
+ } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) {
+ final byte[] newBuffer = new byte[buffer.length + getEOL().length];
+ System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
+ System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length);
+ return newBuffer;
+ } else {
+ return buffer;
+ }
+ }
+
+ private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ return getLabel("a/" + path1, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ return getLabel("a/" + copyFromPath, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ return getLabel("/dev/null", revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ return getLabel("a/" + path1, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ return getLabel("a/" + copyFromPath, revision);
+ }
+ throw new IllegalArgumentException("Unsupported operation: " + operationKind);
+ }
+
+ private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ return getLabel("/dev/null", revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ return getLabel("b/" + path2, revision);
+ }
+ throw new IllegalArgumentException("Unsupported operation: " + operationKind);
+ }
+
+ private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath);
+ }
+ }
+
+ private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "new file mode 10644");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "deleted file mode 10644");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, copyFromPath);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "copy from ");
+ displayString(outputStream, copyFromPath);
+ displayEOL(outputStream);
+ displayString(outputStream, "copy to ");
+ displayString(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, copyFromPath);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "rename from ");
+ displayString(outputStream, copyFromPath);
+ displayEOL(outputStream);
+ displayString(outputStream, "rename to ");
+ displayString(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException {
+ displayGitPath(outputStream, path1, "a/", false);
+ }
+
+ private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException {
+ displayGitPath(outputStream, path2, "b/", false);
+ }
+
+ private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException {
+ String pathPrefix = "a/";
+ if (operation == SvnDiffCallback.OperationKind.Added) {
+ path1 = "/dev/null";
+ pathPrefix = "";
+ }
+ displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true);
+ }
+
+ private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException {
+ String pathPrefix = "b/";
+ if (operation == SvnDiffCallback.OperationKind.Deleted) {
+ path2 = "/dev/null";
+ pathPrefix = "";
+ }
+ displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true);
+ }
+
+ private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException {
+// if (!label && path1.length() == 0) {
+// displayString(outputStream, ".");
+// } else {
+ displayString(outputStream, pathPrefix);
+ displayString(outputStream, path1);
+// }
+ }
+
+ private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) {
+ String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor);
+ return getLabel(adjustedPath, revision);
+ }
+
+ private String getAdjustedPath(String displayPath, String path1, String commonAncestor) {
+ String adjustedPath = getRelativePath(path1, commonAncestor);
+
+ if (adjustedPath == null || adjustedPath.length() == 0) {
+ adjustedPath = displayPath;
+ } else if (adjustedPath.charAt(0) == '/') {
+ adjustedPath = displayPath + "\t(..." + adjustedPath + ")";
+ } else {
+ adjustedPath = displayPath + "\t(.../" + adjustedPath + ")";
+ }
+ return adjustedPath;
+ //TODO: respect relativeToDir
+ }
+
+ protected String getLabel(String path, String revToken) {
+ revToken = revToken == null ? WC_REVISION_LABEL : revToken;
+ return path + "\t" + revToken;
+ }
+
+ protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException {
+ try {
+ if (deleted && !isDiffDeleted()) {
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayString(os, " (deleted)");
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return true;
+ }
+ if (added && !isDiffAdded()) {
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayString(os, " (added)");
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return true;
+ }
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return false;
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ return false;
+ }
+
+ protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException {
+ try {
+ displayString(os, "--- ");
+ displayString(os, label1);
+ displayEOL(os);
+ displayString(os, "+++ ");
+ displayString(os, label2);
+ displayEOL(os);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException {
+ try {
+ displayEOL(outputStream);
+ displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path)));
+ displayEOL(outputStream);
+ displayString(outputStream, PROPERTIES_SEPARATOR);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) {
+ if (value == null) {
+ return null;
+ }
+ if (value.isString()) {
+ try {
+ return value.getString().getBytes(encoding);
+ } catch (UnsupportedEncodingException e) {
+ return value.getString().getBytes();
+ }
+ }
+ return value.getBytes();
+ }
+
+ private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException {
+ Map oldMergeInfo = null;
+ Map newMergeInfo = null;
+ if (oldValue != null) {
+ oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null);
+ }
+ if (newValue != null) {
+ newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null);
+ }
+
+ Map deleted = new TreeMap();
+ Map added = new TreeMap();
+ SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true);
+
+ for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) {
+ String path = (String) paths.next();
+ SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path);
+ displayString(outputStream, (" Reverse-merged " + path + ":r"));
+ displayString(outputStream, rangeList.toString());
+ displayEOL(outputStream);
+ }
+
+ for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) {
+ String path = (String) paths.next();
+ SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path);
+ displayString(outputStream, (" Merged " + path + ":r"));
+ displayString(outputStream, rangeList.toString());
+ displayEOL(outputStream);
+ }
+ }
+
+ private boolean useLocalFileSeparatorChar() {
+ return true;
+ }
+
+ public boolean isDiffDeleted() {
+ return diffDeleted;
+ }
+
+ public boolean isDiffAdded() {
+ return diffAdded;
+ }
+
+ private void wrapException(IOException e) throws SVNException {
+ SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e);
+ SVNErrorManager.error(errorMessage, e, SVNLogType.WC);
+ }
+
+ private void displayString(OutputStream outputStream, String s) throws IOException {
+ outputStream.write(s.getBytes(HEADER_ENCODING));
+ }
+
+ private void displayEOL(OutputStream os) throws IOException {
+ os.write(getEOL());
+ }
+
+ public SVNDiffOptions getDiffOptions() {
+ if (diffOptions == null) {
+ diffOptions = new SVNDiffOptions();
+ }
+ return diffOptions;
+ }
+
+ public void setExternalDiffCommand(String externalDiffCommand) {
+ this.externalDiffCommand = externalDiffCommand;
+ }
+
+ public void setRawDiffOptions(List rawDiffOptions) {
+ this.rawDiffOptions = rawDiffOptions;
+ }
+
+ public void setDiffOptions(SVNDiffOptions diffOptions) {
+ this.diffOptions = diffOptions;
+ }
+
+ public void setDiffDeleted(boolean diffDeleted) {
+ this.diffDeleted = diffDeleted;
+ }
+
+ public void setDiffAdded(boolean diffAdded) {
+ this.diffAdded = diffAdded;
+ }
+
+ public void setBasePath(File absoluteFile) {
+ setBaseTarget(SvnTarget.fromFile(absoluteFile));
+ }
+
+ public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) {
+ this.fallbackToAbsolutePath = fallbackToAbsolutePath;
+ }
+
+ public void setOptions(ISVNOptions options) {
+ this.options = options;
+ }
+
+ public ISVNOptions getOptions() {
+ return options;
+ }
+
+ private class EmptyDetectionOutputStream extends OutputStream {
+
+ private final OutputStream outputStream;
+ private boolean somethingWritten;
+
+ public EmptyDetectionOutputStream(OutputStream outputStream) {
+ this.outputStream = outputStream;
+ this.somethingWritten = false;
+ }
+
+ public boolean isSomethingWritten() {
+ return somethingWritten;
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ somethingWritten = true;
+ outputStream.write(c);
+ }
+
+ @Override
+ public void write(byte[] bytes) throws IOException {
+ somethingWritten = bytes.length > 0;
+ outputStream.write(bytes);
+ }
+
+ @Override
+ public void write(byte[] bytes, int offset, int length) throws IOException {
+ somethingWritten = length > 0;
+ outputStream.write(bytes, offset, length);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ outputStream.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ outputStream.close();
+ }
+ }
+}
diff --git a/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest
new file mode 100644
index 0000000000..c46ce5f534
--- /dev/null
+++ b/scm-it/src/test/resources/diff/largefile/modified/v2/SvnDiffGenerator_forTest
@@ -0,0 +1,1234 @@
+package sonia.scm.repository.spi;
+
+import de.regnis.q.sequence.line.diff.QDiffGenerator;
+import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory;
+import de.regnis.q.sequence.line.diff.QDiffManager;
+import de.regnis.q.sequence.line.diff.QDiffUniGenerator;
+import org.tmatesoft.svn.core.SVNErrorCode;
+import org.tmatesoft.svn.core.SVNErrorMessage;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.SVNMergeRangeList;
+import org.tmatesoft.svn.core.SVNProperties;
+import org.tmatesoft.svn.core.SVNProperty;
+import org.tmatesoft.svn.core.SVNPropertyValue;
+import org.tmatesoft.svn.core.internal.util.SVNHashMap;
+import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
+import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
+import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
+import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback;
+import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
+import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
+import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator;
+import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback;
+import org.tmatesoft.svn.core.wc.ISVNOptions;
+import org.tmatesoft.svn.core.wc.SVNDiffOptions;
+import org.tmatesoft.svn.core.wc2.SvnTarget;
+import org.tmatesoft.svn.util.SVNLogType;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.RandomAccessFile;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+public class SCMSvnDiffGenerator implements ISvnDiffGenerator {
+
+ protected static final String WC_REVISION_LABEL = "(working copy)";
+ protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________";
+ protected static final String HEADER_SEPARATOR = "===================================================================";
+ protected static final String HEADER_ENCODING = "UTF-8";
+
+ private SvnTarget originalTarget1;
+ private SvnTarget originalTarget2;
+ private SvnTarget baseTarget;
+ private SvnTarget relativeToTarget;
+ private SvnTarget repositoryRoot;
+ private String encoding;
+ private byte[] eol;
+ private boolean useGitFormat;
+ private boolean forcedBinaryDiff;
+
+ private boolean diffDeleted;
+ private boolean diffAdded;
+ private List rawDiffOptions;
+ private boolean forceEmpty;
+
+ private Set visitedPaths;
+ private String externalDiffCommand;
+ private SVNDiffOptions diffOptions;
+ private boolean fallbackToAbsolutePath;
+ private ISVNOptions options;
+ private boolean propertiesOnly;
+ private boolean ignoreProperties;
+
+ private String getDisplayPath(SvnTarget target) {
+ String relativePath;
+ if (baseTarget == null) {
+ relativePath = null;
+ } else {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = baseTarget.getPathOrUrlDecodedString();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ }
+
+ return relativePath != null ? relativePath : target.getPathOrUrlString();
+ }
+
+ private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) {
+ String relativePath;
+ if (repositoryRoot == null) {
+ relativePath = null;
+ } else {
+ if (repositoryRoot.isFile() == target.isFile()) {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = repositoryRoot.getPathOrUrlDecodedString();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ } else {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = new File("").getAbsolutePath();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ }
+ }
+
+ return relativePath != null ? relativePath : target.getPathOrUrlString();
+ }
+
+ private String getRelativePath(String targetString, String baseTargetString) {
+ if (targetString != null) {
+ targetString = targetString.replace(File.separatorChar, '/');
+ }
+ if (baseTargetString != null) {
+ baseTargetString = baseTargetString.replace(File.separatorChar, '/');
+ }
+
+ final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString);
+ if (pathAsChild != null) {
+ return pathAsChild;
+ }
+ if (targetString.equals(baseTargetString)) {
+ return "";
+ }
+ return null;
+ }
+
+ private String getChildPath(String path, String relativeToPath) {
+ if (relativeToTarget == null) {
+ return null;
+ }
+
+ String relativePath = getRelativePath(path, relativeToPath);
+ if (relativePath == null) {
+ return path;
+ }
+
+ if (relativePath.length() > 0) {
+ return relativePath;
+ }
+
+ if (relativeToPath.equals(path)) {
+ return ".";
+ }
+
+ return null;
+ }
+
+ public SCMSvnDiffGenerator() {
+ this.originalTarget1 = null;
+ this.originalTarget2 = null;
+ this.visitedPaths = new HashSet();
+ this.diffDeleted = true;
+ this.diffAdded = true;
+ }
+
+ public void setBaseTarget(SvnTarget baseTarget) {
+ this.baseTarget = baseTarget;
+ }
+
+ public void setUseGitFormat(boolean useGitFormat) {
+ this.useGitFormat = useGitFormat;
+ }
+
+ public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) {
+ this.originalTarget1 = originalTarget1;
+ this.originalTarget2 = originalTarget2;
+ }
+
+ public void setRelativeToTarget(SvnTarget relativeToTarget) {
+ this.relativeToTarget = relativeToTarget;
+ }
+
+ public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) {
+ //anchors are not used
+ }
+
+ public void setRepositoryRoot(SvnTarget repositoryRoot) {
+ this.repositoryRoot = repositoryRoot;
+ }
+
+ public void setForceEmpty(boolean forceEmpty) {
+ this.forceEmpty = forceEmpty;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public String getGlobalEncoding() {
+ ISVNOptions options = getOptions();
+
+ if (options != null && options instanceof DefaultSVNOptions) {
+ DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options;
+ return defaultOptions.getGlobalCharset();
+ }
+ return null;
+ }
+
+ public void setEOL(byte[] eol) {
+ this.eol = eol;
+ }
+
+ public byte[] getEOL() {
+ return eol;
+ }
+
+ public boolean isForcedBinaryDiff() {
+ return forcedBinaryDiff;
+ }
+
+ public void setForcedBinaryDiff(boolean forcedBinaryDiff) {
+ this.forcedBinaryDiff = forcedBinaryDiff;
+ }
+
+ public boolean isPropertiesOnly() {
+ return propertiesOnly;
+ }
+
+ public void setPropertiesOnly(boolean propertiesOnly) {
+ this.propertiesOnly = propertiesOnly;
+ }
+
+ public boolean isIgnoreProperties() {
+ return ignoreProperties;
+ }
+
+ public void setIgnoreProperties(boolean ignoreProperties) {
+ this.ignoreProperties = ignoreProperties;
+ }
+
+ public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException {
+ }
+
+ public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException {
+ }
+
+ public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException {
+ if (isIgnoreProperties()) {
+ return;
+ }
+ if (dirWasAdded && !isDiffAdded()) {
+ return;
+ }
+ ensureEncodingAndEOLSet();
+ String displayPath = getDisplayPath(target);
+
+ String targetString1 = originalTarget1.getPathOrUrlDecodedString();
+ String targetString2 = originalTarget2.getPathOrUrlDecodedString();
+
+ if (displayPath == null || displayPath.length() == 0) {
+ displayPath = ".";
+ }
+
+ if (useGitFormat) {
+ targetString1 = adjustRelativeToReposRoot(targetString1);
+ targetString2 = adjustRelativeToReposRoot(targetString2);
+ }
+
+ String newTargetString = displayPath;
+ String newTargetString1 = targetString1;
+ String newTargetString2 = targetString2;
+
+ String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2);
+ int commonLength = commonAncestor == null ? 0 : commonAncestor.length();
+
+ newTargetString1 = newTargetString1.substring(commonLength);
+ newTargetString2 = newTargetString2.substring(commonLength);
+
+ newTargetString1 = computeLabel(newTargetString, newTargetString1);
+ newTargetString2 = computeLabel(newTargetString, newTargetString2);
+
+ if (relativeToTarget != null) {
+ String relativeToPath = relativeToTarget.getPathOrUrlDecodedString();
+ String absolutePath = target.getPathOrUrlDecodedString();
+
+ String childPath = getChildPath(absolutePath, relativeToPath);
+ if (childPath == null) {
+ throwBadRelativePathException(absolutePath, relativeToPath);
+ }
+ String childPath1 = getChildPath(newTargetString1, relativeToPath);
+ if (childPath1 == null) {
+ throwBadRelativePathException(newTargetString1, relativeToPath);
+ }
+ String childPath2 = getChildPath(newTargetString2, relativeToPath);
+ if (childPath2 == null) {
+ throwBadRelativePathException(newTargetString2, relativeToPath);
+ }
+
+ displayPath = childPath;
+ newTargetString1 = childPath1;
+ newTargetString2 = childPath2;
+ }
+
+ boolean showDiffHeader = !visitedPaths.contains(displayPath);
+ if (showDiffHeader) {
+ String label1 = getLabel(newTargetString1, revision1);
+ String label2 = getLabel(newTargetString2, revision2);
+
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified);
+ visitedPaths.add(displayPath);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+// if (useGitFormat) {
+// String copyFromPath = null;
+// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified;
+// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1);
+// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2);
+// displayGitDiffHeader(outputStream, operationKind,
+// getRelativeToRootPath(target, originalTarget1),
+// getRelativeToRootPath(target, originalTarget2),
+// copyFromPath);
+// }
+
+ if (useGitFormat) {
+ displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null);
+ } else {
+ displayHeaderFields(outputStream, label1, label2);
+ }
+ }
+
+ displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream);
+
+ displayPropDiffValues(outputStream, propChanges, originalProps);
+ }
+
+ private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException {
+ SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''",
+ displayPath, relativeToPath);
+ SVNErrorManager.error(errorMessage, SVNLogType.CLIENT);
+ }
+
+ private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1);
+ String path2 = getRelativeToRootPath(target, originalTarget2);
+
+ try {
+ displayString(outputStream, "--- ");
+ displayFirstGitLabelPath(outputStream, path1, revision1, operation);
+ displayEOL(outputStream);
+ displayString(outputStream, "+++ ");
+ displaySecondGitLabelPath(outputStream, path2, revision2, operation);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private String adjustRelativeToReposRoot(String targetString) {
+ if (repositoryRoot != null) {
+ String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString();
+ String relativePath = getRelativePath(targetString, repositoryRootString);
+ return relativePath == null ? "" : relativePath;
+ }
+ return targetString;
+ }
+
+ private String computeLabel(String targetString, String originalTargetString) {
+ if (originalTargetString.length() == 0) {
+ return targetString;
+ } else if (originalTargetString.charAt(0) == '/') {
+ return targetString + "\t(..." + originalTargetString + ")";
+ } else {
+ return targetString + "\t(.../" + originalTargetString + ")";
+ }
+ }
+
+ public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException {
+ if (isPropertiesOnly()) {
+ return;
+ }
+ ensureEncodingAndEOLSet();
+ String displayPath = getDisplayPath(target);
+
+ String targetString1 = originalTarget1.getPathOrUrlDecodedString();
+ String targetString2 = originalTarget2.getPathOrUrlDecodedString();
+
+ if (useGitFormat) {
+ targetString1 = adjustRelativeToReposRoot(targetString1);
+ targetString2 = adjustRelativeToReposRoot(targetString2);
+ }
+
+ String newTargetString = displayPath;
+ String newTargetString1 = targetString1;
+ String newTargetString2 = targetString2;
+
+ String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2);
+ int commonLength = commonAncestor == null ? 0 : commonAncestor.length();
+
+ newTargetString1 = newTargetString1.substring(commonLength);
+ newTargetString2 = newTargetString2.substring(commonLength);
+
+ newTargetString1 = computeLabel(newTargetString, newTargetString1);
+ newTargetString2 = computeLabel(newTargetString, newTargetString2);
+
+ if (relativeToTarget != null) {
+ String relativeToPath = relativeToTarget.getPathOrUrlDecodedString();
+ String absolutePath = target.getPathOrUrlDecodedString();
+
+ String childPath = getChildPath(absolutePath, relativeToPath);
+ if (childPath == null) {
+ throwBadRelativePathException(absolutePath, relativeToPath);
+ }
+ String childPath1 = getChildPath(newTargetString1, relativeToPath);
+ if (childPath1 == null) {
+ throwBadRelativePathException(newTargetString1, relativeToPath);
+ }
+ String childPath2 = getChildPath(newTargetString2, relativeToPath);
+ if (childPath2 == null) {
+ throwBadRelativePathException(newTargetString2, relativeToPath);
+ }
+
+ displayPath = childPath;
+ newTargetString1 = childPath1;
+ newTargetString2 = childPath2;
+ }
+
+ String label1 = getLabel(newTargetString1, revision1);
+ String label2 = getLabel(newTargetString2, revision2);
+
+ boolean leftIsBinary = false;
+ boolean rightIsBinary = false;
+
+ if (mimeType1 != null) {
+ leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1);
+ }
+ if (mimeType2 != null) {
+ rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2);
+ }
+
+ if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) {
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ visitedPaths.add(displayPath);
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+
+ displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary);
+
+ return;
+ }
+
+ final String diffCommand = getExternalDiffCommand();
+ if (diffCommand != null) {
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ visitedPaths.add(displayPath);
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+ runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2);
+ } else {
+ internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2);
+ }
+ }
+
+ private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException {
+ displayCannotDisplayFileMarkedBinary(outputStream);
+
+ if (leftIsBinary && !rightIsBinary) {
+ displayMimeType(outputStream, mimeType1);
+ } else if (!leftIsBinary && rightIsBinary) {
+ displayMimeType(outputStream, mimeType2);
+ } else if (leftIsBinary && rightIsBinary) {
+ if (mimeType1.equals(mimeType2)) {
+ displayMimeType(outputStream, mimeType1);
+ } else {
+ displayMimeTypes(outputStream, mimeType1, mimeType2);
+ }
+ }
+ }
+
+ private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException {
+ String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath);
+ if (file2 == null && !isDiffDeleted()) {
+ try {
+ displayString(outputStream, header);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ visitedPaths.add(displayPath);
+ return;
+ }
+ if (file1 == null && !isDiffAdded()) {
+ try {
+ displayString(outputStream, header);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ visitedPaths.add(displayPath);
+ return;
+ }
+ String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath);
+
+ RandomAccessFile is1 = null;
+ RandomAccessFile is2 = null;
+ try {
+ is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1);
+ is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2);
+
+ QDiffUniGenerator.setup();
+ Map properties = new SVNHashMap();
+
+ properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle()));
+ properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL()));
+ if (getDiffOptions().isIgnoreAllWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE);
+ } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE);
+ }
+
+ final String diffHeader;
+ if (forceEmpty || useGitFormat) {
+ displayString(outputStream, header);
+ diffHeader = headerFields;
+
+ visitedPaths.add(displayPath);
+ } else {
+ diffHeader = header + headerFields;
+ }
+ QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader);
+ EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream);
+ QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator);
+ if (emptyDetectionOutputStream.isSomethingWritten()) {
+ visitedPaths.add(displayPath);
+ }
+ emptyDetectionOutputStream.flush();
+ } catch (IOException e) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage());
+ SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
+ } finally {
+ SVNFileUtil.closeFile(is1);
+ SVNFileUtil.closeFile(is2);
+ }
+ }
+
+ private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ try {
+ if (useGitFormat) {
+ displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath);
+ } else {
+ displayHeaderFields(byteArrayOutputStream, label1, label2);
+ }
+ } catch (SVNException e) {
+ SVNFileUtil.closeFile(byteArrayOutputStream);
+
+ try {
+ byteArrayOutputStream.writeTo(byteArrayOutputStream);
+ } catch (IOException e1) {
+ }
+
+ throw e;
+ }
+
+ try {
+ byteArrayOutputStream.close();
+ return byteArrayOutputStream.toString(HEADER_ENCODING);
+ } catch (IOException e) {
+ return "";
+ }
+ }
+
+ private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ try {
+ if (!useGitFormat) {
+ displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation);
+ } else {
+ displayGitDiffHeader(byteArrayOutputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ copyFromPath);
+ }
+ } catch (SVNException e) {
+ SVNFileUtil.closeFile(byteArrayOutputStream);
+
+ try {
+ byteArrayOutputStream.writeTo(byteArrayOutputStream);
+ } catch (IOException e1) {
+ }
+
+ throw e;
+ }
+
+ try {
+ byteArrayOutputStream.close();
+ return byteArrayOutputStream.toString(HEADER_ENCODING);
+ } catch (IOException e) {
+ return "";
+ }
+ }
+
+ private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException {
+ final List args = new ArrayList();
+ args.add(diffCommand);
+ if (rawDiffOptions != null) {
+ args.addAll(rawDiffOptions);
+ } else {
+ Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection();
+ args.addAll(svnDiffOptionsCollection);
+ args.add("-u");
+ }
+
+ if (label1 != null) {
+ args.add("-L");
+ args.add(label1);
+ }
+
+ if (label2 != null) {
+ args.add("-L");
+ args.add(label2);
+ }
+
+ boolean tmpFile1 = false;
+ boolean tmpFile2 = false;
+ if (file1 == null) {
+ file1 = SVNFileUtil.createTempFile("svn.", ".tmp");
+ tmpFile1 = true;
+ }
+ if (file2 == null) {
+ file2 = SVNFileUtil.createTempFile("svn.", ".tmp");
+ tmpFile2 = true;
+ }
+
+ String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/');
+ String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/');
+
+ args.add(file1Path);
+ args.add(file2Path);
+ try {
+ final Writer writer = new OutputStreamWriter(outputStream, getEncoding());
+
+ SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true,
+ new ISVNReturnValueCallback() {
+
+ public void handleReturnValue(int returnValue) throws SVNException {
+ if (returnValue != 0 && returnValue != 1) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM,
+ "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)});
+ SVNErrorManager.error(err, SVNLogType.DEFAULT);
+ }
+ }
+
+ public void handleChar(char ch) throws SVNException {
+ try {
+ writer.write(ch);
+ } catch (IOException ioe) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage());
+ SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT);
+ }
+ }
+
+ public boolean isHandleProgramOutput() {
+ return true;
+ }
+ });
+
+ writer.flush();
+ } catch (IOException ioe) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage());
+ SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT);
+ } finally {
+ try {
+ if (tmpFile1) {
+ SVNFileUtil.deleteFile(file1);
+ }
+ if (tmpFile2) {
+ SVNFileUtil.deleteFile(file2);
+ }
+ } catch (SVNException e) {
+ // skip
+ }
+ }
+ }
+
+ private String getExternalDiffCommand() {
+ return externalDiffCommand;
+ }
+
+ private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException {
+ try {
+ displayString(outputStream, SVNProperty.MIME_TYPE);
+ displayString(outputStream, " = ");
+ displayString(outputStream, mimeType);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException {
+ try {
+ displayString(outputStream, SVNProperty.MIME_TYPE);
+ displayString(outputStream, " = (");
+ displayString(outputStream, mimeType1);
+ displayString(outputStream, ", ");
+ displayString(outputStream, mimeType2);
+ displayString(outputStream, ")");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException {
+ try {
+ displayString(outputStream, "Cannot display: file marked as a binary type.");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void ensureEncodingAndEOLSet() {
+ if (getEOL() == null) {
+ setEOL(SVNProperty.EOL_LF_BYTES);
+ }
+ if (getEncoding() == null) {
+ final ISVNOptions options = getOptions();
+ if (options != null && options.getNativeCharset() != null) {
+ setEncoding(options.getNativeCharset());
+ } else {
+ setEncoding("UTF-8");
+ }
+ }
+ }
+
+ private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException {
+ for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) {
+ String name = (String) changedPropNames.next();
+ SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null;
+ SVNPropertyValue newValue = diff.getSVNPropertyValue(name);
+ String headerFormat = null;
+
+ if (originalValue == null) {
+ headerFormat = "Added: ";
+ } else if (newValue == null) {
+ headerFormat = "Deleted: ";
+ } else {
+ headerFormat = "Modified: ";
+ }
+
+ try {
+ displayString(outputStream, (headerFormat + name));
+ displayEOL(outputStream);
+ if (SVNProperty.MERGE_INFO.equals(name)) {
+ displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString());
+ continue;
+ }
+
+ byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding());
+ byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding());
+
+ if (originalValueBytes == null) {
+ originalValueBytes = new byte[0];
+ } else {
+ originalValueBytes = maybeAppendEOL(originalValueBytes);
+ }
+
+ boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 &&
+ (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] ||
+ newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]);
+
+ if (newValueBytes == null) {
+ newValueBytes = new byte[0];
+ } else {
+ newValueBytes = maybeAppendEOL(newValueBytes);
+ }
+
+ QDiffUniGenerator.setup();
+ Map properties = new SVNHashMap();
+
+ properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle()));
+ properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL()));
+ properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##");
+ if (getDiffOptions().isIgnoreAllWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE);
+ } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE);
+ }
+
+ QDiffGenerator generator = new QDiffUniGenerator(properties, "");
+ Writer writer = new OutputStreamWriter(outputStream, getEncoding());
+ QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes),
+ null, writer, generator);
+ writer.flush();
+ if (!newValueHadEol) {
+ displayString(outputStream, "\\ No newline at end of property");
+ displayEOL(outputStream);
+ }
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ }
+
+ private byte[] maybeAppendEOL(byte[] buffer) {
+ if (buffer.length == 0) {
+ return buffer;
+ }
+
+ byte lastByte = buffer[buffer.length - 1];
+ if (lastByte == SVNProperty.EOL_CR_BYTES[0]) {
+ return buffer;
+ } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) {
+ final byte[] newBuffer = new byte[buffer.length + getEOL().length];
+ System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
+ System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length);
+ return newBuffer;
+ } else {
+ return buffer;
+ }
+ }
+
+ private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ return getLabel("a/" + path1, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ return getLabel("a/" + copyFromPath, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ return getLabel("/dev/null", revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ return getLabel("a/" + path1, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ return getLabel("a/" + copyFromPath, revision);
+ }
+ throw new IllegalArgumentException("Unsupported operation: " + operationKind);
+ }
+
+ private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ return getLabel("/dev/null", revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ return getLabel("b/" + path2, revision);
+ }
+ throw new IllegalArgumentException("Unsupported operation: " + operationKind);
+ }
+
+ private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath);
+ }
+ }
+
+ private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "new file mode 100644");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "deleted file mode 100644");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, copyFromPath);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "copy from ");
+ displayString(outputStream, copyFromPath);
+ displayEOL(outputStream);
+ displayString(outputStream, "copy to ");
+ displayString(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, copyFromPath);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "rename from ");
+ displayString(outputStream, copyFromPath);
+ displayEOL(outputStream);
+ displayString(outputStream, "rename to ");
+ displayString(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException {
+ displayGitPath(outputStream, path1, "a/", false);
+ }
+
+ private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException {
+ displayGitPath(outputStream, path2, "b/", false);
+ }
+
+ private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException {
+ String pathPrefix = "a/";
+ if (operation == SvnDiffCallback.OperationKind.Added) {
+ path1 = "/dev/null";
+ pathPrefix = "";
+ }
+ displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true);
+ }
+
+ private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException {
+ String pathPrefix = "b/";
+ if (operation == SvnDiffCallback.OperationKind.Deleted) {
+ path2 = "/dev/null";
+ pathPrefix = "";
+ }
+ displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true);
+ }
+
+ private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException {
+// if (!label && path1.length() == 0) {
+// displayString(outputStream, ".");
+// } else {
+ displayString(outputStream, pathPrefix);
+ displayString(outputStream, path1);
+// }
+ }
+
+ private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) {
+ String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor);
+ return getLabel(adjustedPath, revision);
+ }
+
+ private String getAdjustedPath(String displayPath, String path1, String commonAncestor) {
+ String adjustedPath = getRelativePath(path1, commonAncestor);
+
+ if (adjustedPath == null || adjustedPath.length() == 0) {
+ adjustedPath = displayPath;
+ } else if (adjustedPath.charAt(0) == '/') {
+ adjustedPath = displayPath + "\t(..." + adjustedPath + ")";
+ } else {
+ adjustedPath = displayPath + "\t(.../" + adjustedPath + ")";
+ }
+ return adjustedPath;
+ //TODO: respect relativeToDir
+ }
+
+ protected String getLabel(String path, String revToken) {
+ if (useGitFormat){
+ return path;
+ }
+ revToken = revToken == null ? WC_REVISION_LABEL : revToken;
+ return path + "\t" + revToken;
+ }
+
+ protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException {
+ try {
+ if (deleted && !isDiffDeleted()) {
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayString(os, " (deleted)");
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return true;
+ }
+ if (added && !isDiffAdded()) {
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayString(os, " (added)");
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return true;
+ }
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return false;
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ return false;
+ }
+
+ protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException {
+ try {
+ displayString(os, "--- ");
+ displayString(os, label1);
+ displayEOL(os);
+ displayString(os, "+++ ");
+ displayString(os, label2);
+ displayEOL(os);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException {
+ try {
+ displayEOL(outputStream);
+ displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path)));
+ displayEOL(outputStream);
+ displayString(outputStream, PROPERTIES_SEPARATOR);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) {
+ if (value == null) {
+ return null;
+ }
+ if (value.isString()) {
+ try {
+ return value.getString().getBytes(encoding);
+ } catch (UnsupportedEncodingException e) {
+ return value.getString().getBytes();
+ }
+ }
+ return value.getBytes();
+ }
+
+ private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException {
+ Map oldMergeInfo = null;
+ Map newMergeInfo = null;
+ if (oldValue != null) {
+ oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null);
+ }
+ if (newValue != null) {
+ newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null);
+ }
+
+ Map deleted = new TreeMap();
+ Map added = new TreeMap();
+ SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true);
+
+ for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) {
+ String path = (String) paths.next();
+ SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path);
+ displayString(outputStream, (" Reverse-merged " + path + ":r"));
+ displayString(outputStream, rangeList.toString());
+ displayEOL(outputStream);
+ }
+
+ for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) {
+ String path = (String) paths.next();
+ SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path);
+ displayString(outputStream, (" Merged " + path + ":r"));
+ displayString(outputStream, rangeList.toString());
+ displayEOL(outputStream);
+ }
+ }
+
+ private boolean useLocalFileSeparatorChar() {
+ return true;
+ }
+
+ public boolean isDiffDeleted() {
+ return diffDeleted;
+ }
+
+ public boolean isDiffAdded() {
+ return diffAdded;
+ }
+
+ private void wrapException(IOException e) throws SVNException {
+ SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e);
+ SVNErrorManager.error(errorMessage, e, SVNLogType.WC);
+ }
+
+ private void displayString(OutputStream outputStream, String s) throws IOException {
+ outputStream.write(s.getBytes(HEADER_ENCODING));
+ }
+
+ private void displayEOL(OutputStream os) throws IOException {
+ os.write(getEOL());
+ }
+
+ public SVNDiffOptions getDiffOptions() {
+ if (diffOptions == null) {
+ diffOptions = new SVNDiffOptions();
+ }
+ return diffOptions;
+ }
+
+ public void setExternalDiffCommand(String externalDiffCommand) {
+ this.externalDiffCommand = externalDiffCommand;
+ }
+
+ public void setRawDiffOptions(List rawDiffOptions) {
+ this.rawDiffOptions = rawDiffOptions;
+ }
+
+ public void setDiffOptions(SVNDiffOptions diffOptions) {
+ this.diffOptions = diffOptions;
+ }
+
+ public void setDiffDeleted(boolean diffDeleted) {
+ this.diffDeleted = diffDeleted;
+ }
+
+ public void setDiffAdded(boolean diffAdded) {
+ this.diffAdded = diffAdded;
+ }
+
+ public void setBasePath(File absoluteFile) {
+ setBaseTarget(SvnTarget.fromFile(absoluteFile));
+ }
+
+ public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) {
+ this.fallbackToAbsolutePath = fallbackToAbsolutePath;
+ }
+
+ public void setOptions(ISVNOptions options) {
+ this.options = options;
+ }
+
+ public ISVNOptions getOptions() {
+ return options;
+ }
+
+ private class EmptyDetectionOutputStream extends OutputStream {
+
+ private final OutputStream outputStream;
+ private boolean somethingWritten;
+
+ public EmptyDetectionOutputStream(OutputStream outputStream) {
+ this.outputStream = outputStream;
+ this.somethingWritten = false;
+ }
+
+ public boolean isSomethingWritten() {
+ return somethingWritten;
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ somethingWritten = true;
+ outputStream.write(c);
+ }
+
+ @Override
+ public void write(byte[] bytes) throws IOException {
+ somethingWritten = bytes.length > 0;
+ outputStream.write(bytes);
+ }
+
+ @Override
+ public void write(byte[] bytes, int offset, int length) throws IOException {
+ somethingWritten = length > 0;
+ outputStream.write(bytes, offset, length);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ outputStream.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ outputStream.close();
+ }
+ }
+}
diff --git a/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest b/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest
new file mode 100644
index 0000000000..d0cf749024
--- /dev/null
+++ b/scm-it/src/test/resources/diff/largefile/original/SvnDiffGenerator_forTest
@@ -0,0 +1,1240 @@
+package sonia.scm.repository.spi;
+
+import de.regnis.q.sequence.line.diff.QDiffGenerator;
+import de.regnis.q.sequence.line.diff.QDiffGeneratorFactory;
+import de.regnis.q.sequence.line.diff.QDiffManager;
+import de.regnis.q.sequence.line.diff.QDiffUniGenerator;
+import org.tmatesoft.svn.core.SVNErrorCode;
+import org.tmatesoft.svn.core.SVNErrorMessage;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.SVNMergeRangeList;
+import org.tmatesoft.svn.core.SVNProperties;
+import org.tmatesoft.svn.core.SVNProperty;
+import org.tmatesoft.svn.core.SVNPropertyValue;
+import org.tmatesoft.svn.core.internal.util.SVNHashMap;
+import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
+import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
+import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
+import org.tmatesoft.svn.core.internal.wc.ISVNReturnValueCallback;
+import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
+import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
+import org.tmatesoft.svn.core.internal.wc2.ng.ISvnDiffGenerator;
+import org.tmatesoft.svn.core.internal.wc2.ng.SvnDiffCallback;
+import org.tmatesoft.svn.core.wc.ISVNOptions;
+import org.tmatesoft.svn.core.wc.SVNDiffOptions;
+import org.tmatesoft.svn.core.wc2.SvnTarget;
+import org.tmatesoft.svn.util.SVNLogType;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.RandomAccessFile;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+public class SCMSvnDiffGenerator implements ISvnDiffGenerator {
+
+ protected static final String WC_REVISION_LABEL = "(working copy)";
+ protected static final String PROPERTIES_SEPARATOR = "___________________________________________________________________";
+ protected static final String HEADER_SEPARATOR = "===================================================================";
+ protected static final String HEADER_ENCODING = "UTF-8";
+
+ private SvnTarget originalTarget1;
+ private SvnTarget originalTarget2;
+ private SvnTarget baseTarget;
+ private SvnTarget relativeToTarget;
+ private SvnTarget repositoryRoot;
+ private String encoding;
+ private byte[] eol;
+ private boolean useGitFormat;
+ private boolean forcedBinaryDiff;
+
+ private boolean diffDeleted;
+ private boolean diffAdded;
+ private List rawDiffOptions;
+ private boolean forceEmpty;
+
+ private Set visitedPaths;
+ private String externalDiffCommand;
+ private SVNDiffOptions diffOptions;
+ private boolean fallbackToAbsolutePath;
+ private ISVNOptions options;
+ private boolean propertiesOnly;
+ private boolean ignoreProperties;
+
+ private String getDisplayPath(SvnTarget target) {
+ String relativePath;
+ if (baseTarget == null) {
+ relativePath = null;
+ } else {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = baseTarget.getPathOrUrlDecodedString();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ }
+
+ return relativePath != null ? relativePath : target.getPathOrUrlString();
+ }
+
+ private String getRelativeToRootPath(SvnTarget target, SvnTarget originalTarget) {
+ String relativePath;
+ if (repositoryRoot == null)
+ {
+ relativePath = null;
+ }
+ else
+ {
+ if (repositoryRoot.isFile() == target.isFile())
+ {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = repositoryRoot.getPathOrUrlDecodedString();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ }
+ else
+ {
+ String targetString = target.getPathOrUrlDecodedString();
+ String baseTargetString = new File("").getAbsolutePath();
+ relativePath = getRelativePath(targetString, baseTargetString);
+ }
+ }
+
+ return relativePath != null ? relativePath : target.getPathOrUrlString();
+ }
+
+ private String getRelativePath(String targetString, String baseTargetString) {
+ if (targetString != null)
+ {
+ targetString = targetString.replace(File.separatorChar, '/');
+ }
+ if (baseTargetString != null)
+ {
+ baseTargetString = baseTargetString.replace(File.separatorChar, '/');
+ }
+
+ final String pathAsChild = SVNPathUtil.getPathAsChild(baseTargetString, targetString);
+ if (pathAsChild != null)
+ {
+ return pathAsChild;
+ }
+ if (targetString.equals(baseTargetString))
+ {
+ return "";
+ }
+ return null;
+ }
+
+ private String getChildPath(String path, String relativeToPath) {
+ if (relativeToTarget == null) {
+ return null;
+ }
+
+ String relativePath = getRelativePath(path, relativeToPath);
+ if (relativePath == null) {
+ return path;
+ }
+
+ if (relativePath.length() > 0) {
+ return relativePath;
+ }
+
+ if (relativeToPath.equals(path)) {
+ return ".";
+ }
+
+ return null;
+ }
+
+ public SCMSvnDiffGenerator() {
+ this.originalTarget1 = null;
+ this.originalTarget2 = null;
+ this.visitedPaths = new HashSet();
+ this.diffDeleted = true;
+ this.diffAdded = true;
+ }
+
+ public void setBaseTarget(SvnTarget baseTarget) {
+ this.baseTarget = baseTarget;
+ }
+
+ public void setUseGitFormat(boolean useGitFormat) {
+ this.useGitFormat = useGitFormat;
+ }
+
+ public void setOriginalTargets(SvnTarget originalTarget1, SvnTarget originalTarget2) {
+ this.originalTarget1 = originalTarget1;
+ this.originalTarget2 = originalTarget2;
+ }
+
+ public void setRelativeToTarget(SvnTarget relativeToTarget) {
+ this.relativeToTarget = relativeToTarget;
+ }
+
+ public void setAnchors(SvnTarget originalTarget1, SvnTarget originalTarget2) {
+ //anchors are not used
+ }
+
+ public void setRepositoryRoot(SvnTarget repositoryRoot) {
+ this.repositoryRoot = repositoryRoot;
+ }
+
+ public void setForceEmpty(boolean forceEmpty) {
+ this.forceEmpty = forceEmpty;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public String getGlobalEncoding() {
+ ISVNOptions options = getOptions();
+
+ if (options != null && options instanceof DefaultSVNOptions) {
+ DefaultSVNOptions defaultOptions = (DefaultSVNOptions) options;
+ return defaultOptions.getGlobalCharset();
+ }
+ return null;
+ }
+
+ public void setEOL(byte[] eol) {
+ this.eol = eol;
+ }
+
+ public byte[] getEOL() {
+ return eol;
+ }
+
+ public boolean isForcedBinaryDiff() {
+ return forcedBinaryDiff;
+ }
+
+ public void setForcedBinaryDiff(boolean forcedBinaryDiff) {
+ this.forcedBinaryDiff = forcedBinaryDiff;
+ }
+
+ public boolean isPropertiesOnly() {
+ return propertiesOnly;
+ }
+
+ public void setPropertiesOnly(boolean propertiesOnly) {
+ this.propertiesOnly = propertiesOnly;
+ }
+
+ public boolean isIgnoreProperties() {
+ return ignoreProperties;
+ }
+
+ public void setIgnoreProperties(boolean ignoreProperties) {
+ this.ignoreProperties = ignoreProperties;
+ }
+
+ public void displayDeletedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException {
+ }
+
+ public void displayAddedDirectory(SvnTarget target, String revision1, String revision2, OutputStream outputStream) throws SVNException {
+ }
+
+ public void displayPropsChanged(SvnTarget target, String revision1, String revision2, boolean dirWasAdded, SVNProperties originalProps, SVNProperties propChanges, OutputStream outputStream) throws SVNException {
+ if (isIgnoreProperties()) {
+ return;
+ }
+ if (dirWasAdded && !isDiffAdded()) {
+ return;
+ }
+ ensureEncodingAndEOLSet();
+ String displayPath = getDisplayPath(target);
+
+ String targetString1 = originalTarget1.getPathOrUrlDecodedString();
+ String targetString2 = originalTarget2.getPathOrUrlDecodedString();
+
+ if (displayPath == null || displayPath.length() == 0) {
+ displayPath = ".";
+ }
+
+ if (useGitFormat) {
+ targetString1 = adjustRelativeToReposRoot(targetString1);
+ targetString2 = adjustRelativeToReposRoot(targetString2);
+ }
+
+ String newTargetString = displayPath;
+ String newTargetString1 = targetString1;
+ String newTargetString2 = targetString2;
+
+ String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2);
+ int commonLength = commonAncestor == null ? 0 : commonAncestor.length();
+
+ newTargetString1 = newTargetString1.substring(commonLength);
+ newTargetString2 = newTargetString2.substring(commonLength);
+
+ newTargetString1 = computeLabel(newTargetString, newTargetString1);
+ newTargetString2 = computeLabel(newTargetString, newTargetString2);
+
+ if (relativeToTarget != null) {
+ String relativeToPath = relativeToTarget.getPathOrUrlDecodedString();
+ String absolutePath = target.getPathOrUrlDecodedString();
+
+ String childPath = getChildPath(absolutePath, relativeToPath);
+ if (childPath == null) {
+ throwBadRelativePathException(absolutePath, relativeToPath);
+ }
+ String childPath1 = getChildPath(newTargetString1, relativeToPath);
+ if (childPath1 == null) {
+ throwBadRelativePathException(newTargetString1, relativeToPath);
+ }
+ String childPath2 = getChildPath(newTargetString2, relativeToPath);
+ if (childPath2 == null) {
+ throwBadRelativePathException(newTargetString2, relativeToPath);
+ }
+
+ displayPath = childPath;
+ newTargetString1 = childPath1;
+ newTargetString2 = childPath2;
+ }
+
+ boolean showDiffHeader = !visitedPaths.contains(displayPath);
+ if (showDiffHeader) {
+ String label1 = getLabel(newTargetString1, revision1);
+ String label2 = getLabel(newTargetString2, revision2);
+
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, false, fallbackToAbsolutePath, SvnDiffCallback.OperationKind.Modified);
+ visitedPaths.add(displayPath);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, SvnDiffCallback.OperationKind.Modified,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+// if (useGitFormat) {
+// String copyFromPath = null;
+// SvnDiffCallback.OperationKind operationKind = SvnDiffCallback.OperationKind.Modified;
+// label1 = getGitDiffLabel1(operationKind, targetString1, targetString2, copyFromPath, revision1);
+// label2 = getGitDiffLabel2(operationKind, targetString1, targetString2, copyFromPath, revision2);
+// displayGitDiffHeader(outputStream, operationKind,
+// getRelativeToRootPath(target, originalTarget1),
+// getRelativeToRootPath(target, originalTarget2),
+// copyFromPath);
+// }
+
+ if (useGitFormat) {
+ displayGitHeaderFields(outputStream, target, revision1, revision2, SvnDiffCallback.OperationKind.Modified, null);
+ } else {
+ displayHeaderFields(outputStream, label1, label2);
+ }
+ }
+
+ displayPropertyChangesOn(useGitFormat ? getRelativeToRootPath(target, originalTarget1) : displayPath, outputStream);
+
+ displayPropDiffValues(outputStream, propChanges, originalProps);
+ }
+
+ private void throwBadRelativePathException(String displayPath, String relativeToPath) throws SVNException {
+ SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.BAD_RELATIVE_PATH, "Path ''{0}'' must be an immediate child of the directory ''{0}''",
+ displayPath, relativeToPath);
+ SVNErrorManager.error(errorMessage, SVNLogType.CLIENT);
+ }
+
+ private void displayGitHeaderFields(OutputStream outputStream, SvnTarget target, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ String path1 = copyFromPath != null ? copyFromPath : getRelativeToRootPath(target, originalTarget1);
+ String path2 = getRelativeToRootPath(target, originalTarget2);
+
+ try {
+ displayString(outputStream, "--- ");
+ displayFirstGitLabelPath(outputStream, path1, revision1, operation);
+ displayEOL(outputStream);
+ displayString(outputStream, "+++ ");
+ displaySecondGitLabelPath(outputStream, path2, revision2, operation);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private String adjustRelativeToReposRoot(String targetString) {
+ if (repositoryRoot != null) {
+ String repositoryRootString = repositoryRoot.getPathOrUrlDecodedString();
+ String relativePath = getRelativePath(targetString, repositoryRootString);
+ return relativePath == null ? "" : relativePath;
+ }
+ return targetString;
+ }
+
+ private String computeLabel(String targetString, String originalTargetString) {
+ if (originalTargetString.length() == 0) {
+ return targetString;
+ } else if (originalTargetString.charAt(0) == '/') {
+ return targetString + "\t(..." + originalTargetString + ")";
+ } else {
+ return targetString + "\t(.../" + originalTargetString + ")";
+ }
+ }
+
+ public void displayContentChanged(SvnTarget target, File leftFile, File rightFile, String revision1, String revision2, String mimeType1, String mimeType2, SvnDiffCallback.OperationKind operation, File copyFromPath, SVNProperties originalProperties, SVNProperties propChanges, OutputStream outputStream) throws SVNException {
+ if (isPropertiesOnly()) {
+ return;
+ }
+ ensureEncodingAndEOLSet();
+ String displayPath = getDisplayPath(target);
+
+ String targetString1 = originalTarget1.getPathOrUrlDecodedString();
+ String targetString2 = originalTarget2.getPathOrUrlDecodedString();
+
+ if (useGitFormat) {
+ targetString1 = adjustRelativeToReposRoot(targetString1);
+ targetString2 = adjustRelativeToReposRoot(targetString2);
+ }
+
+ String newTargetString = displayPath;
+ String newTargetString1 = targetString1;
+ String newTargetString2 = targetString2;
+
+ String commonAncestor = SVNPathUtil.getCommonPathAncestor(newTargetString1, newTargetString2);
+ int commonLength = commonAncestor == null ? 0 : commonAncestor.length();
+
+ newTargetString1 = newTargetString1.substring(commonLength);
+ newTargetString2 = newTargetString2.substring(commonLength);
+
+ newTargetString1 = computeLabel(newTargetString, newTargetString1);
+ newTargetString2 = computeLabel(newTargetString, newTargetString2);
+
+ if (relativeToTarget != null) {
+ String relativeToPath = relativeToTarget.getPathOrUrlDecodedString();
+ String absolutePath = target.getPathOrUrlDecodedString();
+
+ String childPath = getChildPath(absolutePath, relativeToPath);
+ if (childPath == null) {
+ throwBadRelativePathException(absolutePath, relativeToPath);
+ }
+ String childPath1 = getChildPath(newTargetString1, relativeToPath);
+ if (childPath1 == null) {
+ throwBadRelativePathException(newTargetString1, relativeToPath);
+ }
+ String childPath2 = getChildPath(newTargetString2, relativeToPath);
+ if (childPath2 == null) {
+ throwBadRelativePathException(newTargetString2, relativeToPath);
+ }
+
+ displayPath = childPath;
+ newTargetString1 = childPath1;
+ newTargetString2 = childPath2;
+ }
+
+ String label1 = getLabel(newTargetString1, revision1);
+ String label2 = getLabel(newTargetString2, revision2);
+
+ boolean leftIsBinary = false;
+ boolean rightIsBinary = false;
+
+ if (mimeType1 != null) {
+ leftIsBinary = SVNProperty.isBinaryMimeType(mimeType1);
+ }
+ if (mimeType2 != null) {
+ rightIsBinary = SVNProperty.isBinaryMimeType(mimeType2);
+ }
+
+ if (!forcedBinaryDiff && (leftIsBinary || rightIsBinary)) {
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ visitedPaths.add(displayPath);
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+
+ displayBinary(mimeType1, mimeType2, outputStream, leftIsBinary, rightIsBinary);
+
+ return;
+ }
+
+ final String diffCommand = getExternalDiffCommand();
+ if (diffCommand != null) {
+ boolean shouldStopDisplaying = displayHeader(outputStream, displayPath, rightFile == null, leftFile == null, operation);
+ if (useGitFormat) {
+ displayGitDiffHeader(outputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ null);
+ }
+ visitedPaths.add(displayPath);
+ if (shouldStopDisplaying) {
+ return;
+ }
+
+ runExternalDiffCommand(outputStream, diffCommand, leftFile, rightFile, label1, label2);
+ } else {
+ internalDiff(target, outputStream, displayPath, leftFile, rightFile, label1, label2, operation, copyFromPath == null ? null : copyFromPath.getPath(), revision1, revision2);
+ }
+ }
+
+ private void displayBinary(String mimeType1, String mimeType2, OutputStream outputStream, boolean leftIsBinary, boolean rightIsBinary) throws SVNException {
+ displayCannotDisplayFileMarkedBinary(outputStream);
+
+ if (leftIsBinary && !rightIsBinary) {
+ displayMimeType(outputStream, mimeType1);
+ } else if (!leftIsBinary && rightIsBinary) {
+ displayMimeType(outputStream, mimeType2);
+ } else if (leftIsBinary && rightIsBinary) {
+ if (mimeType1.equals(mimeType2)) {
+ displayMimeType(outputStream, mimeType1);
+ } else {
+ displayMimeTypes(outputStream, mimeType1, mimeType2);
+ }
+ }
+ }
+
+ private void internalDiff(SvnTarget target, OutputStream outputStream, String displayPath, File file1, File file2, String label1, String label2, SvnDiffCallback.OperationKind operation, String copyFromPath, String revision1, String revision2) throws SVNException {
+ String header = getHeaderString(target, displayPath, file2 == null, file1 == null, operation, copyFromPath);
+ if (file2 == null && !isDiffDeleted()) {
+ try {
+ displayString(outputStream, header);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ visitedPaths.add(displayPath);
+ return;
+ }
+ if (file1 == null && !isDiffAdded()) {
+ try {
+ displayString(outputStream, header);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ visitedPaths.add(displayPath);
+ return;
+ }
+ String headerFields = getHeaderFieldsString(target, displayPath, label1, label2, revision1, revision2, operation, copyFromPath);
+
+ RandomAccessFile is1 = null;
+ RandomAccessFile is2 = null;
+ try {
+ is1 = file1 == null ? null : SVNFileUtil.openRAFileForReading(file1);
+ is2 = file2 == null ? null : SVNFileUtil.openRAFileForReading(file2);
+
+ QDiffUniGenerator.setup();
+ Map properties = new SVNHashMap();
+
+ properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle()));
+ properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL()));
+ if (getDiffOptions().isIgnoreAllWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE);
+ } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE);
+ }
+
+ final String diffHeader;
+ if (forceEmpty || useGitFormat) {
+ displayString(outputStream, header);
+ diffHeader = headerFields;
+
+ visitedPaths.add(displayPath);
+ } else {
+ diffHeader = header + headerFields;
+ }
+ QDiffGenerator generator = new QDiffUniGenerator(properties, diffHeader);
+ EmptyDetectionOutputStream emptyDetectionOutputStream = new EmptyDetectionOutputStream(outputStream);
+ QDiffManager.generateTextDiff(is1, is2, emptyDetectionOutputStream, generator);
+ if (emptyDetectionOutputStream.isSomethingWritten()) {
+ visitedPaths.add(displayPath);
+ }
+ emptyDetectionOutputStream.flush();
+ } catch (IOException e) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage());
+ SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
+ } finally {
+ SVNFileUtil.closeFile(is1);
+ SVNFileUtil.closeFile(is2);
+ }
+ }
+
+ private String getHeaderFieldsString(SvnTarget target, String displayPath, String label1, String label2, String revision1, String revision2, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ try {
+ if (useGitFormat) {
+ displayGitHeaderFields(byteArrayOutputStream, target, revision1, revision2, operation, copyFromPath);
+ } else {
+ displayHeaderFields(byteArrayOutputStream, label1, label2);
+ }
+ } catch (SVNException e) {
+ SVNFileUtil.closeFile(byteArrayOutputStream);
+
+ try {
+ byteArrayOutputStream.writeTo(byteArrayOutputStream);
+ } catch (IOException e1) {
+ }
+
+ throw e;
+ }
+
+ try {
+ byteArrayOutputStream.close();
+ return byteArrayOutputStream.toString(HEADER_ENCODING);
+ } catch (IOException e) {
+ return "";
+ }
+ }
+
+ private String getHeaderString(SvnTarget target, String displayPath, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation, String copyFromPath) throws SVNException {
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ try {
+ boolean stopDisplaying = displayHeader(byteArrayOutputStream, displayPath, deleted, added, operation);
+ if (useGitFormat) {
+ displayGitDiffHeader(byteArrayOutputStream, operation,
+ getRelativeToRootPath(target, originalTarget1),
+ getRelativeToRootPath(target, originalTarget2),
+ copyFromPath);
+ }
+ } catch (SVNException e) {
+ SVNFileUtil.closeFile(byteArrayOutputStream);
+
+ try {
+ byteArrayOutputStream.writeTo(byteArrayOutputStream);
+ } catch (IOException e1) {
+ }
+
+ throw e;
+ }
+
+ try {
+ byteArrayOutputStream.close();
+ return byteArrayOutputStream.toString(HEADER_ENCODING);
+ } catch (IOException e) {
+ return "";
+ }
+ }
+
+ private void runExternalDiffCommand(OutputStream outputStream, final String diffCommand, File file1, File file2, String label1, String label2) throws SVNException {
+ final List args = new ArrayList();
+ args.add(diffCommand);
+ if (rawDiffOptions != null) {
+ args.addAll(rawDiffOptions);
+ } else {
+ Collection svnDiffOptionsCollection = getDiffOptions().toOptionsCollection();
+ args.addAll(svnDiffOptionsCollection);
+ args.add("-u");
+ }
+
+ if (label1 != null) {
+ args.add("-L");
+ args.add(label1);
+ }
+
+ if (label2 != null) {
+ args.add("-L");
+ args.add(label2);
+ }
+
+ boolean tmpFile1 = false;
+ boolean tmpFile2 = false;
+ if (file1 == null) {
+ file1 = SVNFileUtil.createTempFile("svn.", ".tmp");
+ tmpFile1 = true;
+ }
+ if (file2 == null) {
+ file2 = SVNFileUtil.createTempFile("svn.", ".tmp");
+ tmpFile2 = true;
+ }
+
+ String file1Path = file1.getAbsolutePath().replace(File.separatorChar, '/');
+ String file2Path = file2.getAbsolutePath().replace(File.separatorChar, '/');
+
+ args.add(file1Path);
+ args.add(file2Path);
+ try {
+ final Writer writer = new OutputStreamWriter(outputStream, getEncoding());
+
+ SVNFileUtil.execCommand(args.toArray(new String[args.size()]), true,
+ new ISVNReturnValueCallback() {
+
+ public void handleReturnValue(int returnValue) throws SVNException {
+ if (returnValue != 0 && returnValue != 1) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.EXTERNAL_PROGRAM,
+ "''{0}'' returned {1}", new Object[]{diffCommand, String.valueOf(returnValue)});
+ SVNErrorManager.error(err, SVNLogType.DEFAULT);
+ }
+ }
+
+ public void handleChar(char ch) throws SVNException {
+ try {
+ writer.write(ch);
+ } catch (IOException ioe) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage());
+ SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT);
+ }
+ }
+
+ public boolean isHandleProgramOutput() {
+ return true;
+ }
+ });
+
+ writer.flush();
+ } catch (IOException ioe) {
+ SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage());
+ SVNErrorManager.error(err, ioe, SVNLogType.DEFAULT);
+ } finally {
+ try {
+ if (tmpFile1) {
+ SVNFileUtil.deleteFile(file1);
+ }
+ if (tmpFile2) {
+ SVNFileUtil.deleteFile(file2);
+ }
+ } catch (SVNException e) {
+ // skip
+ }
+ }
+ }
+
+ private String getExternalDiffCommand() {
+ return externalDiffCommand;
+ }
+
+ private void displayMimeType(OutputStream outputStream, String mimeType) throws SVNException {
+ try {
+ displayString(outputStream, SVNProperty.MIME_TYPE);
+ displayString(outputStream, " = ");
+ displayString(outputStream, mimeType);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayMimeTypes(OutputStream outputStream, String mimeType1, String mimeType2) throws SVNException {
+ try {
+ displayString(outputStream, SVNProperty.MIME_TYPE);
+ displayString(outputStream, " = (");
+ displayString(outputStream, mimeType1);
+ displayString(outputStream, ", ");
+ displayString(outputStream, mimeType2);
+ displayString(outputStream, ")");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayCannotDisplayFileMarkedBinary(OutputStream outputStream) throws SVNException {
+ try {
+ displayString(outputStream, "Cannot display: file marked as a binary type.");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void ensureEncodingAndEOLSet() {
+ if (getEOL() == null) {
+ setEOL(SVNProperty.EOL_LF_BYTES);
+ }
+ if (getEncoding() == null) {
+ final ISVNOptions options = getOptions();
+ if (options != null && options.getNativeCharset() != null) {
+ setEncoding(options.getNativeCharset());
+ } else {
+ setEncoding("UTF-8");
+ }
+ }
+ }
+
+ private void displayPropDiffValues(OutputStream outputStream, SVNProperties diff, SVNProperties baseProps) throws SVNException {
+ for (Iterator changedPropNames = diff.nameSet().iterator(); changedPropNames.hasNext(); ) {
+ String name = (String) changedPropNames.next();
+ SVNPropertyValue originalValue = baseProps != null ? baseProps.getSVNPropertyValue(name) : null;
+ SVNPropertyValue newValue = diff.getSVNPropertyValue(name);
+ String headerFormat = null;
+
+ if (originalValue == null) {
+ headerFormat = "Added: ";
+ } else if (newValue == null) {
+ headerFormat = "Deleted: ";
+ } else {
+ headerFormat = "Modified: ";
+ }
+
+ try {
+ displayString(outputStream, (headerFormat + name));
+ displayEOL(outputStream);
+ if (SVNProperty.MERGE_INFO.equals(name)) {
+ displayMergeInfoDiff(outputStream, originalValue == null ? null : originalValue.getString(), newValue == null ? null : newValue.getString());
+ continue;
+ }
+
+ byte[] originalValueBytes = getPropertyAsBytes(originalValue, getEncoding());
+ byte[] newValueBytes = getPropertyAsBytes(newValue, getEncoding());
+
+ if (originalValueBytes == null) {
+ originalValueBytes = new byte[0];
+ } else {
+ originalValueBytes = maybeAppendEOL(originalValueBytes);
+ }
+
+ boolean newValueHadEol = newValueBytes != null && newValueBytes.length > 0 &&
+ (newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_CR_BYTES[0] ||
+ newValueBytes[newValueBytes.length - 1] == SVNProperty.EOL_LF_BYTES[0]);
+
+ if (newValueBytes == null) {
+ newValueBytes = new byte[0];
+ } else {
+ newValueBytes = maybeAppendEOL(newValueBytes);
+ }
+
+ QDiffUniGenerator.setup();
+ Map properties = new SVNHashMap();
+
+ properties.put(QDiffGeneratorFactory.IGNORE_EOL_PROPERTY, Boolean.valueOf(getDiffOptions().isIgnoreEOLStyle()));
+ properties.put(QDiffGeneratorFactory.EOL_PROPERTY, new String(getEOL()));
+ properties.put(QDiffGeneratorFactory.HUNK_DELIMITER, "##");
+ if (getDiffOptions().isIgnoreAllWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_ALL_SPACE);
+ } else if (getDiffOptions().isIgnoreAmountOfWhitespace()) {
+ properties.put(QDiffGeneratorFactory.IGNORE_SPACE_PROPERTY, QDiffGeneratorFactory.IGNORE_SPACE_CHANGE);
+ }
+
+ QDiffGenerator generator = new QDiffUniGenerator(properties, "");
+ Writer writer = new OutputStreamWriter(outputStream, getEncoding());
+ QDiffManager.generateTextDiff(new ByteArrayInputStream(originalValueBytes), new ByteArrayInputStream(newValueBytes),
+ null, writer, generator);
+ writer.flush();
+ if (!newValueHadEol) {
+ displayString(outputStream, "\\ No newline at end of property");
+ displayEOL(outputStream);
+ }
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ }
+
+ private byte[] maybeAppendEOL(byte[] buffer) {
+ if (buffer.length == 0) {
+ return buffer;
+ }
+
+ byte lastByte = buffer[buffer.length - 1];
+ if (lastByte == SVNProperty.EOL_CR_BYTES[0]) {
+ return buffer;
+ } else if (lastByte != SVNProperty.EOL_LF_BYTES[0]) {
+ final byte[] newBuffer = new byte[buffer.length + getEOL().length];
+ System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
+ System.arraycopy(getEOL(), 0, newBuffer, buffer.length, getEOL().length);
+ return newBuffer;
+ } else {
+ return buffer;
+ }
+ }
+
+ private String getGitDiffLabel1(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ return getLabel("a/" + path1, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ return getLabel("a/" + copyFromPath, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ return getLabel("/dev/null", revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ return getLabel("a/" + path1, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ return getLabel("a/" + copyFromPath, revision);
+ }
+ throw new IllegalArgumentException("Unsupported operation: " + operationKind);
+ }
+
+ private String getGitDiffLabel2(SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath, String revision) {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ return getLabel("/dev/null", revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ return getLabel("b/" + path2, revision);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ return getLabel("b/" + path2, revision);
+ }
+ throw new IllegalArgumentException("Unsupported operation: " + operationKind);
+ }
+
+ private void displayGitDiffHeader(OutputStream outputStream, SvnDiffCallback.OperationKind operationKind, String path1, String path2, String copyFromPath) throws SVNException {
+ if (operationKind == SvnDiffCallback.OperationKind.Deleted) {
+ displayGitDiffHeaderDeleted(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Copied) {
+ displayGitDiffHeaderCopied(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Added) {
+ displayGitDiffHeaderAdded(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Modified) {
+ displayGitDiffHeaderModified(outputStream, path1, path2, copyFromPath);
+ } else if (operationKind == SvnDiffCallback.OperationKind.Moved) {
+ displayGitDiffHeaderRenamed(outputStream, path1, path2, copyFromPath);
+ }
+ }
+
+ private void displayGitDiffHeaderAdded(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "new file mode 10644");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderDeleted(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "deleted file mode 10644");
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderCopied(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, copyFromPath);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "copy from ");
+ displayString(outputStream, copyFromPath);
+ displayEOL(outputStream);
+ displayString(outputStream, "copy to ");
+ displayString(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderRenamed(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, copyFromPath);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ displayString(outputStream, "rename from ");
+ displayString(outputStream, copyFromPath);
+ displayEOL(outputStream);
+ displayString(outputStream, "rename to ");
+ displayString(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayGitDiffHeaderModified(OutputStream outputStream, String path1, String path2, String copyFromPath) throws SVNException {
+ try {
+ displayString(outputStream, "diff --git ");
+ displayFirstGitPath(outputStream, path1);
+ displayString(outputStream, " ");
+ displaySecondGitPath(outputStream, path2);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayFirstGitPath(OutputStream outputStream, String path1) throws IOException {
+ displayGitPath(outputStream, path1, "a/", false);
+ }
+
+ private void displaySecondGitPath(OutputStream outputStream, String path2) throws IOException {
+ displayGitPath(outputStream, path2, "b/", false);
+ }
+
+ private void displayFirstGitLabelPath(OutputStream outputStream, String path1, String revision1, SvnDiffCallback.OperationKind operation) throws IOException {
+ String pathPrefix = "a/";
+ if (operation == SvnDiffCallback.OperationKind.Added) {
+ path1 = "/dev/null";
+ pathPrefix = "";
+ }
+ displayGitPath(outputStream, getLabel(path1, revision1), pathPrefix, true);
+ }
+
+ private void displaySecondGitLabelPath(OutputStream outputStream, String path2, String revision2, SvnDiffCallback.OperationKind operation) throws IOException {
+ String pathPrefix = "b/";
+ if (operation == SvnDiffCallback.OperationKind.Deleted) {
+ path2 = "/dev/null";
+ pathPrefix = "";
+ }
+ displayGitPath(outputStream, getLabel(path2, revision2), pathPrefix, true);
+ }
+
+ private void displayGitPath(OutputStream outputStream, String path1, String pathPrefix, boolean label) throws IOException {
+// if (!label && path1.length() == 0) {
+// displayString(outputStream, ".");
+// } else {
+ displayString(outputStream, pathPrefix);
+ displayString(outputStream, path1);
+// }
+ }
+
+ private String getAdjustedPathWithLabel(String displayPath, String path, String revision, String commonAncestor) {
+ String adjustedPath = getAdjustedPath(displayPath, path, commonAncestor);
+ return getLabel(adjustedPath, revision);
+ }
+
+ private String getAdjustedPath(String displayPath, String path1, String commonAncestor) {
+ String adjustedPath = getRelativePath(path1, commonAncestor);
+
+ if (adjustedPath == null || adjustedPath.length() == 0) {
+ adjustedPath = displayPath;
+ } else if (adjustedPath.charAt(0) == '/') {
+ adjustedPath = displayPath + "\t(..." + adjustedPath + ")";
+ } else {
+ adjustedPath = displayPath + "\t(.../" + adjustedPath + ")";
+ }
+ return adjustedPath;
+ //TODO: respect relativeToDir
+ }
+
+ protected String getLabel(String path, String revToken) {
+ revToken = revToken == null ? WC_REVISION_LABEL : revToken;
+ return path + "\t" + revToken;
+ }
+
+ protected boolean displayHeader(OutputStream os, String path, boolean deleted, boolean added, SvnDiffCallback.OperationKind operation) throws SVNException {
+ try {
+ if (deleted && !isDiffDeleted()) {
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayString(os, " (deleted)");
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return true;
+ }
+ if (added && !isDiffAdded()) {
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayString(os, " (added)");
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return true;
+ }
+ displayString(os, "Index: ");
+ displayString(os, path);
+ displayEOL(os);
+ displayString(os, HEADER_SEPARATOR);
+ displayEOL(os);
+ return false;
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ return false;
+ }
+
+ protected void displayHeaderFields(OutputStream os, String label1, String label2) throws SVNException {
+ try {
+ displayString(os, "--- ");
+ displayString(os, label1);
+ displayEOL(os);
+ displayString(os, "+++ ");
+ displayString(os, label2);
+ displayEOL(os);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private void displayPropertyChangesOn(String path, OutputStream outputStream) throws SVNException {
+ try {
+ displayEOL(outputStream);
+ displayString(outputStream, ("Property changes on: " + (useLocalFileSeparatorChar() ? path.replace('/', File.separatorChar) : path)));
+ displayEOL(outputStream);
+ displayString(outputStream, PROPERTIES_SEPARATOR);
+ displayEOL(outputStream);
+ } catch (IOException e) {
+ wrapException(e);
+ }
+ }
+
+ private byte[] getPropertyAsBytes(SVNPropertyValue value, String encoding) {
+ if (value == null) {
+ return null;
+ }
+ if (value.isString()) {
+ try {
+ return value.getString().getBytes(encoding);
+ } catch (UnsupportedEncodingException e) {
+ return value.getString().getBytes();
+ }
+ }
+ return value.getBytes();
+ }
+
+ private void displayMergeInfoDiff(OutputStream outputStream, String oldValue, String newValue) throws SVNException, IOException {
+ Map oldMergeInfo = null;
+ Map newMergeInfo = null;
+ if (oldValue != null) {
+ oldMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(oldValue), null);
+ }
+ if (newValue != null) {
+ newMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(newValue), null);
+ }
+
+ Map deleted = new TreeMap();
+ Map added = new TreeMap();
+ SVNMergeInfoUtil.diffMergeInfo(deleted, added, oldMergeInfo, newMergeInfo, true);
+
+ for (Iterator paths = deleted.keySet().iterator(); paths.hasNext(); ) {
+ String path = (String) paths.next();
+ SVNMergeRangeList rangeList = (SVNMergeRangeList) deleted.get(path);
+ displayString(outputStream, (" Reverse-merged " + path + ":r"));
+ displayString(outputStream, rangeList.toString());
+ displayEOL(outputStream);
+ }
+
+ for (Iterator paths = added.keySet().iterator(); paths.hasNext(); ) {
+ String path = (String) paths.next();
+ SVNMergeRangeList rangeList = (SVNMergeRangeList) added.get(path);
+ displayString(outputStream, (" Merged " + path + ":r"));
+ displayString(outputStream, rangeList.toString());
+ displayEOL(outputStream);
+ }
+ }
+
+ private boolean useLocalFileSeparatorChar() {
+ return true;
+ }
+
+ public boolean isDiffDeleted() {
+ return diffDeleted;
+ }
+
+ public boolean isDiffAdded() {
+ return diffAdded;
+ }
+
+ private void wrapException(IOException e) throws SVNException {
+ SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, e);
+ SVNErrorManager.error(errorMessage, e, SVNLogType.WC);
+ }
+
+ private void displayString(OutputStream outputStream, String s) throws IOException {
+ outputStream.write(s.getBytes(HEADER_ENCODING));
+ }
+
+ private void displayEOL(OutputStream os) throws IOException {
+ os.write(getEOL());
+ }
+
+ public SVNDiffOptions getDiffOptions() {
+ if (diffOptions == null) {
+ diffOptions = new SVNDiffOptions();
+ }
+ return diffOptions;
+ }
+
+ public void setExternalDiffCommand(String externalDiffCommand) {
+ this.externalDiffCommand = externalDiffCommand;
+ }
+
+ public void setRawDiffOptions(List rawDiffOptions) {
+ this.rawDiffOptions = rawDiffOptions;
+ }
+
+ public void setDiffOptions(SVNDiffOptions diffOptions) {
+ this.diffOptions = diffOptions;
+ }
+
+ public void setDiffDeleted(boolean diffDeleted) {
+ this.diffDeleted = diffDeleted;
+ }
+
+ public void setDiffAdded(boolean diffAdded) {
+ this.diffAdded = diffAdded;
+ }
+
+ public void setBasePath(File absoluteFile) {
+ setBaseTarget(SvnTarget.fromFile(absoluteFile));
+ }
+
+ public void setFallbackToAbsolutePath(boolean fallbackToAbsolutePath) {
+ this.fallbackToAbsolutePath = fallbackToAbsolutePath;
+ }
+
+ public void setOptions(ISVNOptions options) {
+ this.options = options;
+ }
+
+ public ISVNOptions getOptions() {
+ return options;
+ }
+
+ private class EmptyDetectionOutputStream extends OutputStream {
+
+ private final OutputStream outputStream;
+ private boolean somethingWritten;
+
+ public EmptyDetectionOutputStream(OutputStream outputStream) {
+ this.outputStream = outputStream;
+ this.somethingWritten = false;
+ }
+
+ public boolean isSomethingWritten() {
+ return somethingWritten;
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ somethingWritten = true;
+ outputStream.write(c);
+ }
+
+ @Override
+ public void write(byte[] bytes) throws IOException {
+ somethingWritten = bytes.length > 0;
+ outputStream.write(bytes);
+ }
+
+ @Override
+ public void write(byte[] bytes, int offset, int length) throws IOException {
+ somethingWritten = length > 0;
+ outputStream.write(bytes, offset, length);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ outputStream.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ outputStream.close();
+ }
+ }
+}
diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml
index 16a74549e5..db0173d1df 100644
--- a/scm-plugins/pom.xml
+++ b/scm-plugins/pom.xml
@@ -113,92 +113,29 @@
sonia.scm.maven
smp-maven-plugin
- 1.0.0-alpha-2
+ 1.0.0-alpha-3
true
-
- true
-
-
-
- fix-descriptor
- process-resources
-
- fix-descriptor
-
-
-
- append-dependencies
- process-classes
-
- append-dependencies
-
-
-
-
- com.github.sdorra
- buildfrontend-maven-plugin
-
-
- ${nodejs.version}
-
-
- YARN
- ${yarn.version}
-
- false
-
-
-
-
- install
- process-resources
-
- install
-
-
-
- build
- compile
-
- run
-
-
-
-
+
+ com.github.sdorra
+ buildfrontend-maven-plugin
+
+
+ ${nodejs.version}
+
+
+ YARN
+ ${yarn.version}
+
+ false
+
+
-
- release
-
-
-
-
- sonia.maven
- web-compressor
- 1.4
-
-
- compile
-
- compress-directory
-
-
-
-
- true
- ${project.build.directory}/classes
-
-
-
-
-
-
-
doc
diff --git a/scm-plugins/scm-git-plugin/.flowconfig b/scm-plugins/scm-git-plugin/.flowconfig
index 7ede008602..b05e157358 100644
--- a/scm-plugins/scm-git-plugin/.flowconfig
+++ b/scm-plugins/scm-git-plugin/.flowconfig
@@ -4,5 +4,6 @@
[include]
[libs]
+./node_modules/@scm-manager/ui-components/flow-typed
[options]
diff --git a/scm-plugins/scm-git-plugin/flow-typed/npm/classnames_v2.x.x.js b/scm-plugins/scm-git-plugin/flow-typed/npm/classnames_v2.x.x.js
deleted file mode 100644
index 2307243eeb..0000000000
--- a/scm-plugins/scm-git-plugin/flow-typed/npm/classnames_v2.x.x.js
+++ /dev/null
@@ -1,23 +0,0 @@
-// flow-typed signature: cf86673cc32d185bdab1d2ea90578d37
-// flow-typed version: 614bf49aa8/classnames_v2.x.x/flow_>=v0.25.x
-
-type $npm$classnames$Classes =
- | string
- | { [className: string]: * }
- | false
- | void
- | null;
-
-declare module "classnames" {
- declare module.exports: (
- ...classes: Array<$npm$classnames$Classes | $npm$classnames$Classes[]>
- ) => string;
-}
-
-declare module "classnames/bind" {
- declare module.exports: $Exports<"classnames">;
-}
-
-declare module "classnames/dedupe" {
- declare module.exports: $Exports<"classnames">;
-}
diff --git a/scm-plugins/scm-git-plugin/flow-typed/npm/jest_v23.x.x.js b/scm-plugins/scm-git-plugin/flow-typed/npm/jest_v23.x.x.js
deleted file mode 100644
index 23b66b07e5..0000000000
--- a/scm-plugins/scm-git-plugin/flow-typed/npm/jest_v23.x.x.js
+++ /dev/null
@@ -1,1108 +0,0 @@
-// flow-typed signature: f5a484315a3dea13d273645306e4076a
-// flow-typed version: 7c5d14b3d4/jest_v23.x.x/flow_>=v0.39.x
-
-type JestMockFn, TReturn> = {
- (...args: TArguments): TReturn,
- /**
- * An object for introspecting mock calls
- */
- mock: {
- /**
- * An array that represents all calls that have been made into this mock
- * function. Each call is represented by an array of arguments that were
- * passed during the call.
- */
- calls: Array,
- /**
- * An array that contains all the object instances that have been
- * instantiated from this mock function.
- */
- instances: Array
- },
- /**
- * Resets all information stored in the mockFn.mock.calls and
- * mockFn.mock.instances arrays. Often this is useful when you want to clean
- * up a mock's usage data between two assertions.
- */
- mockClear(): void,
- /**
- * Resets all information stored in the mock. This is useful when you want to
- * completely restore a mock back to its initial state.
- */
- mockReset(): void,
- /**
- * Removes the mock and restores the initial implementation. This is useful
- * when you want to mock functions in certain test cases and restore the
- * original implementation in others. Beware that mockFn.mockRestore only
- * works when mock was created with jest.spyOn. Thus you have to take care of
- * restoration yourself when manually assigning jest.fn().
- */
- mockRestore(): void,
- /**
- * Accepts a function that should be used as the implementation of the mock.
- * The mock itself will still record all calls that go into and instances
- * that come from itself -- the only difference is that the implementation
- * will also be executed when the mock is called.
- */
- mockImplementation(
- fn: (...args: TArguments) => TReturn
- ): JestMockFn,
- /**
- * Accepts a function that will be used as an implementation of the mock for
- * one call to the mocked function. Can be chained so that multiple function
- * calls produce different results.
- */
- mockImplementationOnce(
- fn: (...args: TArguments) => TReturn
- ): JestMockFn,
- /**
- * Accepts a string to use in test result output in place of "jest.fn()" to
- * indicate which mock function is being referenced.
- */
- mockName(name: string): JestMockFn,
- /**
- * Just a simple sugar function for returning `this`
- */
- mockReturnThis(): void,
- /**
- * Accepts a value that will be returned whenever the mock function is called.
- */
- mockReturnValue(value: TReturn): JestMockFn,
- /**
- * Sugar for only returning a value once inside your mock
- */
- mockReturnValueOnce(value: TReturn): JestMockFn,
- /**
- * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value))
- */
- mockResolvedValue(value: TReturn): JestMockFn>,
- /**
- * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value))
- */
- mockResolvedValueOnce(value: TReturn): JestMockFn>,
- /**
- * Sugar for jest.fn().mockImplementation(() => Promise.reject(value))
- */
- mockRejectedValue(value: TReturn): JestMockFn>,
- /**
- * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value))
- */
- mockRejectedValueOnce(value: TReturn): JestMockFn>
-};
-
-type JestAsymmetricEqualityType = {
- /**
- * A custom Jasmine equality tester
- */
- asymmetricMatch(value: mixed): boolean
-};
-
-type JestCallsType = {
- allArgs(): mixed,
- all(): mixed,
- any(): boolean,
- count(): number,
- first(): mixed,
- mostRecent(): mixed,
- reset(): void
-};
-
-type JestClockType = {
- install(): void,
- mockDate(date: Date): void,
- tick(milliseconds?: number): void,
- uninstall(): void
-};
-
-type JestMatcherResult = {
- message?: string | (() => string),
- pass: boolean
-};
-
-type JestMatcher = (actual: any, expected: any) => JestMatcherResult;
-
-type JestPromiseType = {
- /**
- * Use rejects to unwrap the reason of a rejected promise so any other
- * matcher can be chained. If the promise is fulfilled the assertion fails.
- */
- rejects: JestExpectType,
- /**
- * Use resolves to unwrap the value of a fulfilled promise so any other
- * matcher can be chained. If the promise is rejected the assertion fails.
- */
- resolves: JestExpectType
-};
-
-/**
- * Jest allows functions and classes to be used as test names in test() and
- * describe()
- */
-type JestTestName = string | Function;
-
-/**
- * Plugin: jest-styled-components
- */
-
-type JestStyledComponentsMatcherValue =
- | string
- | JestAsymmetricEqualityType
- | RegExp
- | typeof undefined;
-
-type JestStyledComponentsMatcherOptions = {
- media?: string;
- modifier?: string;
- supports?: string;
-}
-
-type JestStyledComponentsMatchersType = {
- toHaveStyleRule(
- property: string,
- value: JestStyledComponentsMatcherValue,
- options?: JestStyledComponentsMatcherOptions
- ): void,
-};
-
-/**
- * Plugin: jest-enzyme
- */
-type EnzymeMatchersType = {
- toBeChecked(): void,
- toBeDisabled(): void,
- toBeEmpty(): void,
- toBeEmptyRender(): void,
- toBePresent(): void,
- toContainReact(element: React$Element): void,
- toExist(): void,
- toHaveClassName(className: string): void,
- toHaveHTML(html: string): void,
- toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void),
- toHaveRef(refName: string): void,
- toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void),
- toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void),
- toHaveTagName(tagName: string): void,
- toHaveText(text: string): void,
- toIncludeText(text: string): void,
- toHaveValue(value: any): void,
- toMatchElement(element: React$Element): void,
- toMatchSelector(selector: string): void
-};
-
-// DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers
-type DomTestingLibraryType = {
- toBeInTheDOM(): void,
- toHaveTextContent(content: string): void,
- toHaveAttribute(name: string, expectedValue?: string): void
-};
-
-// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers
-type JestJQueryMatchersType = {
- toExist(): void,
- toHaveLength(len: number): void,
- toHaveId(id: string): void,
- toHaveClass(className: string): void,
- toHaveTag(tag: string): void,
- toHaveAttr(key: string, val?: any): void,
- toHaveProp(key: string, val?: any): void,
- toHaveText(text: string | RegExp): void,
- toHaveData(key: string, val?: any): void,
- toHaveValue(val: any): void,
- toHaveCss(css: {[key: string]: any}): void,
- toBeChecked(): void,
- toBeDisabled(): void,
- toBeEmpty(): void,
- toBeHidden(): void,
- toBeSelected(): void,
- toBeVisible(): void,
- toBeFocused(): void,
- toBeInDom(): void,
- toBeMatchedBy(sel: string): void,
- toHaveDescendant(sel: string): void,
- toHaveDescendantWithText(sel: string, text: string | RegExp): void
-};
-
-
-// Jest Extended Matchers: https://github.com/jest-community/jest-extended
-type JestExtendedMatchersType = {
- /**
- * Note: Currently unimplemented
- * Passing assertion
- *
- * @param {String} message
- */
- // pass(message: string): void;
-
- /**
- * Note: Currently unimplemented
- * Failing assertion
- *
- * @param {String} message
- */
- // fail(message: string): void;
-
- /**
- * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty.
- */
- toBeEmpty(): void;
-
- /**
- * Use .toBeOneOf when checking if a value is a member of a given Array.
- * @param {Array.<*>} members
- */
- toBeOneOf(members: any[]): void;
-
- /**
- * Use `.toBeNil` when checking a value is `null` or `undefined`.
- */
- toBeNil(): void;
-
- /**
- * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`.
- * @param {Function} predicate
- */
- toSatisfy(predicate: (n: any) => boolean): void;
-
- /**
- * Use `.toBeArray` when checking if a value is an `Array`.
- */
- toBeArray(): void;
-
- /**
- * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x.
- * @param {Number} x
- */
- toBeArrayOfSize(x: number): void;
-
- /**
- * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set.
- * @param {Array.<*>} members
- */
- toIncludeAllMembers(members: any[]): void;
-
- /**
- * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set.
- * @param {Array.<*>} members
- */
- toIncludeAnyMembers(members: any[]): void;
-
- /**
- * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array.
- * @param {Function} predicate
- */
- toSatisfyAll(predicate: (n: any) => boolean): void;
-
- /**
- * Use `.toBeBoolean` when checking if a value is a `Boolean`.
- */
- toBeBoolean(): void;
-
- /**
- * Use `.toBeTrue` when checking a value is equal (===) to `true`.
- */
- toBeTrue(): void;
-
- /**
- * Use `.toBeFalse` when checking a value is equal (===) to `false`.
- */
- toBeFalse(): void;
-
- /**
- * Use .toBeDate when checking if a value is a Date.
- */
- toBeDate(): void;
-
- /**
- * Use `.toBeFunction` when checking if a value is a `Function`.
- */
- toBeFunction(): void;
-
- /**
- * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`.
- *
- * Note: Required Jest version >22
- * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same
- *
- * @param {Mock} mock
- */
- toHaveBeenCalledBefore(mock: JestMockFn): void;
-
- /**
- * Use `.toBeNumber` when checking if a value is a `Number`.
- */
- toBeNumber(): void;
-
- /**
- * Use `.toBeNaN` when checking a value is `NaN`.
- */
- toBeNaN(): void;
-
- /**
- * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`.
- */
- toBeFinite(): void;
-
- /**
- * Use `.toBePositive` when checking if a value is a positive `Number`.
- */
- toBePositive(): void;
-
- /**
- * Use `.toBeNegative` when checking if a value is a negative `Number`.
- */
- toBeNegative(): void;
-
- /**
- * Use `.toBeEven` when checking if a value is an even `Number`.
- */
- toBeEven(): void;
-
- /**
- * Use `.toBeOdd` when checking if a value is an odd `Number`.
- */
- toBeOdd(): void;
-
- /**
- * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive).
- *
- * @param {Number} start
- * @param {Number} end
- */
- toBeWithin(start: number, end: number): void;
-
- /**
- * Use `.toBeObject` when checking if a value is an `Object`.
- */
- toBeObject(): void;
-
- /**
- * Use `.toContainKey` when checking if an object contains the provided key.
- *
- * @param {String} key
- */
- toContainKey(key: string): void;
-
- /**
- * Use `.toContainKeys` when checking if an object has all of the provided keys.
- *
- * @param {Array.} keys
- */
- toContainKeys(keys: string[]): void;
-
- /**
- * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys.
- *
- * @param {Array.} keys
- */
- toContainAllKeys(keys: string[]): void;
-
- /**
- * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys.
- *
- * @param {Array.} keys
- */
- toContainAnyKeys(keys: string[]): void;
-
- /**
- * Use `.toContainValue` when checking if an object contains the provided value.
- *
- * @param {*} value
- */
- toContainValue(value: any): void;
-
- /**
- * Use `.toContainValues` when checking if an object contains all of the provided values.
- *
- * @param {Array.<*>} values
- */
- toContainValues(values: any[]): void;
-
- /**
- * Use `.toContainAllValues` when checking if an object only contains all of the provided values.
- *
- * @param {Array.<*>} values
- */
- toContainAllValues(values: any[]): void;
-
- /**
- * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values.
- *
- * @param {Array.<*>} values
- */
- toContainAnyValues(values: any[]): void;
-
- /**
- * Use `.toContainEntry` when checking if an object contains the provided entry.
- *
- * @param {Array.} entry
- */
- toContainEntry(entry: [string, string]): void;
-
- /**
- * Use `.toContainEntries` when checking if an object contains all of the provided entries.
- *
- * @param {Array.>} entries
- */
- toContainEntries(entries: [string, string][]): void;
-
- /**
- * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries.
- *
- * @param {Array.>} entries
- */
- toContainAllEntries(entries: [string, string][]): void;
-
- /**
- * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries.
- *
- * @param {Array.>} entries
- */
- toContainAnyEntries(entries: [string, string][]): void;
-
- /**
- * Use `.toBeExtensible` when checking if an object is extensible.
- */
- toBeExtensible(): void;
-
- /**
- * Use `.toBeFrozen` when checking if an object is frozen.
- */
- toBeFrozen(): void;
-
- /**
- * Use `.toBeSealed` when checking if an object is sealed.
- */
- toBeSealed(): void;
-
- /**
- * Use `.toBeString` when checking if a value is a `String`.
- */
- toBeString(): void;
-
- /**
- * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings.
- *
- * @param {String} string
- */
- toEqualCaseInsensitive(string: string): void;
-
- /**
- * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix.
- *
- * @param {String} prefix
- */
- toStartWith(prefix: string): void;
-
- /**
- * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix.
- *
- * @param {String} suffix
- */
- toEndWith(suffix: string): void;
-
- /**
- * Use `.toInclude` when checking if a `String` includes the given `String` substring.
- *
- * @param {String} substring
- */
- toInclude(substring: string): void;
-
- /**
- * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times.
- *
- * @param {String} substring
- * @param {Number} times
- */
- toIncludeRepeated(substring: string, times: number): void;
-
- /**
- * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings.
- *
- * @param {Array.} substring
- */
- toIncludeMultiple(substring: string[]): void;
-};
-
-interface JestExpectType {
- not:
- & JestExpectType
- & EnzymeMatchersType
- & DomTestingLibraryType
- & JestJQueryMatchersType
- & JestStyledComponentsMatchersType
- & JestExtendedMatchersType,
- /**
- * If you have a mock function, you can use .lastCalledWith to test what
- * arguments it was last called with.
- */
- lastCalledWith(...args: Array): void,
- /**
- * toBe just checks that a value is what you expect. It uses === to check
- * strict equality.
- */
- toBe(value: any): void,
- /**
- * Use .toBeCalledWith to ensure that a mock function was called with
- * specific arguments.
- */
- toBeCalledWith(...args: Array): void,
- /**
- * Using exact equality with floating point numbers is a bad idea. Rounding
- * means that intuitive things fail.
- */
- toBeCloseTo(num: number, delta: any): void,
- /**
- * Use .toBeDefined to check that a variable is not undefined.
- */
- toBeDefined(): void,
- /**
- * Use .toBeFalsy when you don't care what a value is, you just want to
- * ensure a value is false in a boolean context.
- */
- toBeFalsy(): void,
- /**
- * To compare floating point numbers, you can use toBeGreaterThan.
- */
- toBeGreaterThan(number: number): void,
- /**
- * To compare floating point numbers, you can use toBeGreaterThanOrEqual.
- */
- toBeGreaterThanOrEqual(number: number): void,
- /**
- * To compare floating point numbers, you can use toBeLessThan.
- */
- toBeLessThan(number: number): void,
- /**
- * To compare floating point numbers, you can use toBeLessThanOrEqual.
- */
- toBeLessThanOrEqual(number: number): void,
- /**
- * Use .toBeInstanceOf(Class) to check that an object is an instance of a
- * class.
- */
- toBeInstanceOf(cls: Class<*>): void,
- /**
- * .toBeNull() is the same as .toBe(null) but the error messages are a bit
- * nicer.
- */
- toBeNull(): void,
- /**
- * Use .toBeTruthy when you don't care what a value is, you just want to
- * ensure a value is true in a boolean context.
- */
- toBeTruthy(): void,
- /**
- * Use .toBeUndefined to check that a variable is undefined.
- */
- toBeUndefined(): void,
- /**
- * Use .toContain when you want to check that an item is in a list. For
- * testing the items in the list, this uses ===, a strict equality check.
- */
- toContain(item: any): void,
- /**
- * Use .toContainEqual when you want to check that an item is in a list. For
- * testing the items in the list, this matcher recursively checks the
- * equality of all fields, rather than checking for object identity.
- */
- toContainEqual(item: any): void,
- /**
- * Use .toEqual when you want to check that two objects have the same value.
- * This matcher recursively checks the equality of all fields, rather than
- * checking for object identity.
- */
- toEqual(value: any): void,
- /**
- * Use .toHaveBeenCalled to ensure that a mock function got called.
- */
- toHaveBeenCalled(): void,
- toBeCalled(): void;
- /**
- * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact
- * number of times.
- */
- toHaveBeenCalledTimes(number: number): void,
- toBeCalledTimes(number: number): void;
- /**
- *
- */
- toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void;
- nthCalledWith(nthCall: number, ...args: Array): void;
- /**
- *
- */
- toHaveReturned(): void;
- toReturn(): void;
- /**
- *
- */
- toHaveReturnedTimes(number: number): void;
- toReturnTimes(number: number): void;
- /**
- *
- */
- toHaveReturnedWith(value: any): void;
- toReturnWith(value: any): void;
- /**
- *
- */
- toHaveLastReturnedWith(value: any): void;
- lastReturnedWith(value: any): void;
- /**
- *
- */
- toHaveNthReturnedWith(nthCall: number, value: any): void;
- nthReturnedWith(nthCall: number, value: any): void;
- /**
- * Use .toHaveBeenCalledWith to ensure that a mock function was called with
- * specific arguments.
- */
- toHaveBeenCalledWith(...args: Array): void,
- toBeCalledWith(...args: Array): void,
- /**
- * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called
- * with specific arguments.
- */
- toHaveBeenLastCalledWith(...args: Array): void,
- lastCalledWith(...args: Array): void,
- /**
- * Check that an object has a .length property and it is set to a certain
- * numeric value.
- */
- toHaveLength(number: number): void,
- /**
- *
- */
- toHaveProperty(propPath: string, value?: any): void,
- /**
- * Use .toMatch to check that a string matches a regular expression or string.
- */
- toMatch(regexpOrString: RegExp | string): void,
- /**
- * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object.
- */
- toMatchObject(object: Object | Array): void,
- /**
- * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object.
- */
- toStrictEqual(value: any): void,
- /**
- * This ensures that an Object matches the most recent snapshot.
- */
- toMatchSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, name?: string): void,
- /**
- * This ensures that an Object matches the most recent snapshot.
- */
- toMatchSnapshot(name: string): void,
-
- toMatchInlineSnapshot(snapshot?: string): void,
- toMatchInlineSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, snapshot?: string): void,
- /**
- * Use .toThrow to test that a function throws when it is called.
- * If you want to test that a specific error gets thrown, you can provide an
- * argument to toThrow. The argument can be a string for the error message,
- * a class for the error, or a regex that should match the error.
- *
- * Alias: .toThrowError
- */
- toThrow(message?: string | Error | Class | RegExp): void,
- toThrowError(message?: string | Error | Class | RegExp): void,
- /**
- * Use .toThrowErrorMatchingSnapshot to test that a function throws a error
- * matching the most recent snapshot when it is called.
- */
- toThrowErrorMatchingSnapshot(): void,
- toThrowErrorMatchingInlineSnapshot(snapshot?: string): void,
-}
-
-type JestObjectType = {
- /**
- * Disables automatic mocking in the module loader.
- *
- * After this method is called, all `require()`s will return the real
- * versions of each module (rather than a mocked version).
- */
- disableAutomock(): JestObjectType,
- /**
- * An un-hoisted version of disableAutomock
- */
- autoMockOff(): JestObjectType,
- /**
- * Enables automatic mocking in the module loader.
- */
- enableAutomock(): JestObjectType,
- /**
- * An un-hoisted version of enableAutomock
- */
- autoMockOn(): JestObjectType,
- /**
- * Clears the mock.calls and mock.instances properties of all mocks.
- * Equivalent to calling .mockClear() on every mocked function.
- */
- clearAllMocks(): JestObjectType,
- /**
- * Resets the state of all mocks. Equivalent to calling .mockReset() on every
- * mocked function.
- */
- resetAllMocks(): JestObjectType,
- /**
- * Restores all mocks back to their original value.
- */
- restoreAllMocks(): JestObjectType,
- /**
- * Removes any pending timers from the timer system.
- */
- clearAllTimers(): void,
- /**
- * The same as `mock` but not moved to the top of the expectation by
- * babel-jest.
- */
- doMock(moduleName: string, moduleFactory?: any): JestObjectType,
- /**
- * The same as `unmock` but not moved to the top of the expectation by
- * babel-jest.
- */
- dontMock(moduleName: string): JestObjectType,
- /**
- * Returns a new, unused mock function. Optionally takes a mock
- * implementation.
- */
- fn, TReturn>(
- implementation?: (...args: TArguments) => TReturn
- ): JestMockFn,
- /**
- * Determines if the given function is a mocked function.
- */
- isMockFunction(fn: Function): boolean,
- /**
- * Given the name of a module, use the automatic mocking system to generate a
- * mocked version of the module for you.
- */
- genMockFromModule(moduleName: string): any,
- /**
- * Mocks a module with an auto-mocked version when it is being required.
- *
- * The second argument can be used to specify an explicit module factory that
- * is being run instead of using Jest's automocking feature.
- *
- * The third argument can be used to create virtual mocks -- mocks of modules
- * that don't exist anywhere in the system.
- */
- mock(
- moduleName: string,
- moduleFactory?: any,
- options?: Object
- ): JestObjectType,
- /**
- * Returns the actual module instead of a mock, bypassing all checks on
- * whether the module should receive a mock implementation or not.
- */
- requireActual(moduleName: string): any,
- /**
- * Returns a mock module instead of the actual module, bypassing all checks
- * on whether the module should be required normally or not.
- */
- requireMock(moduleName: string): any,
- /**
- * Resets the module registry - the cache of all required modules. This is
- * useful to isolate modules where local state might conflict between tests.
- */
- resetModules(): JestObjectType,
- /**
- * Exhausts the micro-task queue (usually interfaced in node via
- * process.nextTick).
- */
- runAllTicks(): void,
- /**
- * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(),
- * setInterval(), and setImmediate()).
- */
- runAllTimers(): void,
- /**
- * Exhausts all tasks queued by setImmediate().
- */
- runAllImmediates(): void,
- /**
- * Executes only the macro task queue (i.e. all tasks queued by setTimeout()
- * or setInterval() and setImmediate()).
- */
- advanceTimersByTime(msToRun: number): void,
- /**
- * Executes only the macro task queue (i.e. all tasks queued by setTimeout()
- * or setInterval() and setImmediate()).
- *
- * Renamed to `advanceTimersByTime`.
- */
- runTimersToTime(msToRun: number): void,
- /**
- * Executes only the macro-tasks that are currently pending (i.e., only the
- * tasks that have been queued by setTimeout() or setInterval() up to this
- * point)
- */
- runOnlyPendingTimers(): void,
- /**
- * Explicitly supplies the mock object that the module system should return
- * for the specified module. Note: It is recommended to use jest.mock()
- * instead.
- */
- setMock(moduleName: string, moduleExports: any): JestObjectType,
- /**
- * Indicates that the module system should never return a mocked version of
- * the specified module from require() (e.g. that it should always return the
- * real module).
- */
- unmock(moduleName: string): JestObjectType,
- /**
- * Instructs Jest to use fake versions of the standard timer functions
- * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick,
- * setImmediate and clearImmediate).
- */
- useFakeTimers(): JestObjectType,
- /**
- * Instructs Jest to use the real versions of the standard timer functions.
- */
- useRealTimers(): JestObjectType,
- /**
- * Creates a mock function similar to jest.fn but also tracks calls to
- * object[methodName].
- */
- spyOn(object: Object, methodName: string, accessType?: "get" | "set"): JestMockFn,
- /**
- * Set the default timeout interval for tests and before/after hooks in milliseconds.
- * Note: The default timeout interval is 5 seconds if this method is not called.
- */
- setTimeout(timeout: number): JestObjectType
-};
-
-type JestSpyType = {
- calls: JestCallsType
-};
-
-/** Runs this function after every test inside this context */
-declare function afterEach(
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-/** Runs this function before every test inside this context */
-declare function beforeEach(
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-/** Runs this function after all tests have finished inside this context */
-declare function afterAll(
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-/** Runs this function before any tests have started inside this context */
-declare function beforeAll(
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-
-/** A context for grouping tests together */
-declare var describe: {
- /**
- * Creates a block that groups together several related tests in one "test suite"
- */
- (name: JestTestName, fn: () => void): void,
-
- /**
- * Only run this describe block
- */
- only(name: JestTestName, fn: () => void): void,
-
- /**
- * Skip running this describe block
- */
- skip(name: JestTestName, fn: () => void): void
-};
-
-/** An individual test unit */
-declare var it: {
- /**
- * An individual test unit
- *
- * @param {JestTestName} Name of Test
- * @param {Function} Test
- * @param {number} Timeout for the test, in milliseconds.
- */
- (
- name: JestTestName,
- fn?: (done: () => void) => ?Promise,
- timeout?: number
- ): void,
- /**
- * each runs this test against array of argument arrays per each run
- *
- * @param {table} table of Test
- */
- each(
- table: Array>
- ): (
- name: JestTestName,
- fn?: (...args: Array) => ?Promise
- ) => void,
- /**
- * Only run this test
- *
- * @param {JestTestName} Name of Test
- * @param {Function} Test
- * @param {number} Timeout for the test, in milliseconds.
- */
- only(
- name: JestTestName,
- fn?: (done: () => void) => ?Promise,
- timeout?: number
- ): {
- each(
- table: Array>
- ): (
- name: JestTestName,
- fn?: (...args: Array) => ?Promise
- ) => void,
- },
- /**
- * Skip running this test
- *
- * @param {JestTestName} Name of Test
- * @param {Function} Test
- * @param {number} Timeout for the test, in milliseconds.
- */
- skip(
- name: JestTestName,
- fn?: (done: () => void) => ?Promise,
- timeout?: number
- ): void,
- /**
- * Run the test concurrently
- *
- * @param {JestTestName} Name of Test
- * @param {Function} Test
- * @param {number} Timeout for the test, in milliseconds.
- */
- concurrent(
- name: JestTestName,
- fn?: (done: () => void) => ?Promise,
- timeout?: number
- ): void
-};
-declare function fit(
- name: JestTestName,
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-/** An individual test unit */
-declare var test: typeof it;
-/** A disabled group of tests */
-declare var xdescribe: typeof describe;
-/** A focused group of tests */
-declare var fdescribe: typeof describe;
-/** A disabled individual test */
-declare var xit: typeof it;
-/** A disabled individual test */
-declare var xtest: typeof it;
-
-type JestPrettyFormatColors = {
- comment: { close: string, open: string },
- content: { close: string, open: string },
- prop: { close: string, open: string },
- tag: { close: string, open: string },
- value: { close: string, open: string },
-};
-
-type JestPrettyFormatIndent = string => string;
-type JestPrettyFormatRefs = Array;
-type JestPrettyFormatPrint = any => string;
-type JestPrettyFormatStringOrNull = string | null;
-
-type JestPrettyFormatOptions = {|
- callToJSON: boolean,
- edgeSpacing: string,
- escapeRegex: boolean,
- highlight: boolean,
- indent: number,
- maxDepth: number,
- min: boolean,
- plugins: JestPrettyFormatPlugins,
- printFunctionName: boolean,
- spacing: string,
- theme: {|
- comment: string,
- content: string,
- prop: string,
- tag: string,
- value: string,
- |},
-|};
-
-type JestPrettyFormatPlugin = {
- print: (
- val: any,
- serialize: JestPrettyFormatPrint,
- indent: JestPrettyFormatIndent,
- opts: JestPrettyFormatOptions,
- colors: JestPrettyFormatColors,
- ) => string,
- test: any => boolean,
-};
-
-type JestPrettyFormatPlugins = Array;
-
-/** The expect function is used every time you want to test a value */
-declare var expect: {
- /** The object that you want to make assertions against */
- (value: any):
- & JestExpectType
- & JestPromiseType
- & EnzymeMatchersType
- & DomTestingLibraryType
- & JestJQueryMatchersType
- & JestStyledComponentsMatchersType
- & JestExtendedMatchersType,
-
- /** Add additional Jasmine matchers to Jest's roster */
- extend(matchers: { [name: string]: JestMatcher }): void,
- /** Add a module that formats application-specific data structures. */
- addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void,
- assertions(expectedAssertions: number): void,
- hasAssertions(): void,
- any(value: mixed): JestAsymmetricEqualityType,
- anything(): any,
- arrayContaining(value: Array): Array,
- objectContaining(value: Object): Object,
- /** Matches any received string that contains the exact expected string. */
- stringContaining(value: string): string,
- stringMatching(value: string | RegExp): string,
- not: {
- arrayContaining: (value: $ReadOnlyArray) => Array,
- objectContaining: (value: {}) => Object,
- stringContaining: (value: string) => string,
- stringMatching: (value: string | RegExp) => string,
- },
-};
-
-// TODO handle return type
-// http://jasmine.github.io/2.4/introduction.html#section-Spies
-declare function spyOn(value: mixed, method: string): Object;
-
-/** Holds all functions related to manipulating test runner */
-declare var jest: JestObjectType;
-
-/**
- * The global Jasmine object, this is generally not exposed as the public API,
- * using features inside here could break in later versions of Jest.
- */
-declare var jasmine: {
- DEFAULT_TIMEOUT_INTERVAL: number,
- any(value: mixed): JestAsymmetricEqualityType,
- anything(): any,
- arrayContaining(value: Array): Array,
- clock(): JestClockType,
- createSpy(name: string): JestSpyType,
- createSpyObj(
- baseName: string,
- methodNames: Array
- ): { [methodName: string]: JestSpyType },
- objectContaining(value: Object): Object,
- stringMatching(value: string): string
-};
diff --git a/scm-plugins/scm-git-plugin/flow-typed/npm/moment_v2.3.x.js b/scm-plugins/scm-git-plugin/flow-typed/npm/moment_v2.3.x.js
deleted file mode 100644
index c2786e87fd..0000000000
--- a/scm-plugins/scm-git-plugin/flow-typed/npm/moment_v2.3.x.js
+++ /dev/null
@@ -1,331 +0,0 @@
-// flow-typed signature: 23b805356f90ad9384dd88489654e380
-// flow-typed version: e9374c5fe9/moment_v2.3.x/flow_>=v0.25.x
-
-type moment$MomentOptions = {
- y?: number | string,
- year?: number | string,
- years?: number | string,
- M?: number | string,
- month?: number | string,
- months?: number | string,
- d?: number | string,
- day?: number | string,
- days?: number | string,
- date?: number | string,
- h?: number | string,
- hour?: number | string,
- hours?: number | string,
- m?: number | string,
- minute?: number | string,
- minutes?: number | string,
- s?: number | string,
- second?: number | string,
- seconds?: number | string,
- ms?: number | string,
- millisecond?: number | string,
- milliseconds?: number | string
-};
-
-type moment$MomentObject = {
- years: number,
- months: number,
- date: number,
- hours: number,
- minutes: number,
- seconds: number,
- milliseconds: number
-};
-
-type moment$MomentCreationData = {
- input: string,
- format: string,
- locale: Object,
- isUTC: boolean,
- strict: boolean
-};
-
-type moment$CalendarFormat = string | ((moment: moment$Moment) => string);
-
-type moment$CalendarFormats = {
- sameDay?: moment$CalendarFormat,
- nextDay?: moment$CalendarFormat,
- nextWeek?: moment$CalendarFormat,
- lastDay?: moment$CalendarFormat,
- lastWeek?: moment$CalendarFormat,
- sameElse?: moment$CalendarFormat
-};
-
-declare class moment$LocaleData {
- months(moment: moment$Moment): string,
- monthsShort(moment: moment$Moment): string,
- monthsParse(month: string): number,
- weekdays(moment: moment$Moment): string,
- weekdaysShort(moment: moment$Moment): string,
- weekdaysMin(moment: moment$Moment): string,
- weekdaysParse(weekDay: string): number,
- longDateFormat(dateFormat: string): string,
- isPM(date: string): boolean,
- meridiem(hours: number, minutes: number, isLower: boolean): string,
- calendar(
- key:
- | "sameDay"
- | "nextDay"
- | "lastDay"
- | "nextWeek"
- | "prevWeek"
- | "sameElse",
- moment: moment$Moment
- ): string,
- relativeTime(
- number: number,
- withoutSuffix: boolean,
- key: "s" | "m" | "mm" | "h" | "hh" | "d" | "dd" | "M" | "MM" | "y" | "yy",
- isFuture: boolean
- ): string,
- pastFuture(diff: any, relTime: string): string,
- ordinal(number: number): string,
- preparse(str: string): any,
- postformat(str: string): any,
- week(moment: moment$Moment): string,
- invalidDate(): string,
- firstDayOfWeek(): number,
- firstDayOfYear(): number
-}
-declare class moment$MomentDuration {
- humanize(suffix?: boolean): string,
- milliseconds(): number,
- asMilliseconds(): number,
- seconds(): number,
- asSeconds(): number,
- minutes(): number,
- asMinutes(): number,
- hours(): number,
- asHours(): number,
- days(): number,
- asDays(): number,
- months(): number,
- asWeeks(): number,
- weeks(): number,
- asMonths(): number,
- years(): number,
- asYears(): number,
- add(value: number | moment$MomentDuration | Object, unit?: string): this,
- subtract(value: number | moment$MomentDuration | Object, unit?: string): this,
- as(unit: string): number,
- get(unit: string): number,
- toJSON(): string,
- toISOString(): string,
- isValid(): boolean
-}
-declare class moment$Moment {
- static ISO_8601: string,
- static (
- string?: string,
- format?: string | Array,
- strict?: boolean
- ): moment$Moment,
- static (
- string?: string,
- format?: string | Array,
- locale?: string,
- strict?: boolean
- ): moment$Moment,
- static (
- initDate: ?Object | number | Date | Array | moment$Moment | string
- ): moment$Moment,
- static unix(seconds: number): moment$Moment,
- static utc(): moment$Moment,
- static utc(number: number | Array): moment$Moment,
- static utc(
- str: string,
- str2?: string | Array,
- str3?: string
- ): moment$Moment,
- static utc(moment: moment$Moment): moment$Moment,
- static utc(date: Date): moment$Moment,
- static parseZone(): moment$Moment,
- static parseZone(rawDate: string): moment$Moment,
- static parseZone(
- rawDate: string,
- format: string | Array
- ): moment$Moment,
- static parseZone(
- rawDate: string,
- format: string,
- strict: boolean
- ): moment$Moment,
- static parseZone(
- rawDate: string,
- format: string,
- locale: string,
- strict: boolean
- ): moment$Moment,
- isValid(): boolean,
- invalidAt(): 0 | 1 | 2 | 3 | 4 | 5 | 6,
- creationData(): moment$MomentCreationData,
- millisecond(number: number): this,
- milliseconds(number: number): this,
- millisecond(): number,
- milliseconds(): number,
- second(number: number): this,
- seconds(number: number): this,
- second(): number,
- seconds(): number,
- minute(number: number): this,
- minutes(number: number): this,
- minute(): number,
- minutes(): number,
- hour(number: number): this,
- hours(number: number): this,
- hour(): number,
- hours(): number,
- date(number: number): this,
- dates(number: number): this,
- date(): number,
- dates(): number,
- day(day: number | string): this,
- days(day: number | string): this,
- day(): number,
- days(): number,
- weekday(number: number): this,
- weekday(): number,
- isoWeekday(number: number): this,
- isoWeekday(): number,
- dayOfYear(number: number): this,
- dayOfYear(): number,
- week(number: number): this,
- weeks(number: number): this,
- week(): number,
- weeks(): number,
- isoWeek(number: number): this,
- isoWeeks(number: number): this,
- isoWeek(): number,
- isoWeeks(): number,
- month(number: number): this,
- months(number: number): this,
- month(): number,
- months(): number,
- quarter(number: number): this,
- quarter(): number,
- year(number: number): this,
- years(number: number): this,
- year(): number,
- years(): number,
- weekYear(number: number): this,
- weekYear(): number,
- isoWeekYear(number: number): this,
- isoWeekYear(): number,
- weeksInYear(): number,
- isoWeeksInYear(): number,
- get(string: string): number,
- set(unit: string, value: number): this,
- set(options: { [unit: string]: number }): this,
- static max(...dates: Array): moment$Moment,
- static max(dates: Array): moment$Moment,
- static min(...dates: Array): moment$Moment,
- static min(dates: Array): moment$Moment,
- add(
- value: number | moment$MomentDuration | moment$Moment | Object,
- unit?: string
- ): this,
- subtract(
- value: number | moment$MomentDuration | moment$Moment | string | Object,
- unit?: string
- ): this,
- startOf(unit: string): this,
- endOf(unit: string): this,
- local(): this,
- utc(): this,
- utcOffset(
- offset: number | string,
- keepLocalTime?: boolean,
- keepMinutes?: boolean
- ): this,
- utcOffset(): number,
- format(format?: string): string,
- fromNow(removeSuffix?: boolean): string,
- from(
- value: moment$Moment | string | number | Date | Array,
- removePrefix?: boolean
- ): string,
- toNow(removePrefix?: boolean): string,
- to(
- value: moment$Moment | string | number | Date | Array,
- removePrefix?: boolean
- ): string,
- calendar(refTime?: any, formats?: moment$CalendarFormats): string,
- diff(
- date: moment$Moment | string | number | Date | Array,
- format?: string,
- floating?: boolean
- ): number,
- valueOf(): number,
- unix(): number,
- daysInMonth(): number,
- toDate(): Date,
- toArray(): Array,
- toJSON(): string,
- toISOString(
- keepOffset?: boolean
- ): string,
- toObject(): moment$MomentObject,
- isBefore(
- date?: moment$Moment | string | number | Date | Array,
- units?: ?string
- ): boolean,
- isSame(
- date?: moment$Moment | string | number | Date | Array,
- units?: ?string
- ): boolean,
- isAfter(
- date?: moment$Moment | string | number | Date | Array,
- units?: ?string
- ): boolean,
- isSameOrBefore(
- date?: moment$Moment | string | number | Date | Array,
- units?: ?string
- ): boolean,
- isSameOrAfter(
- date?: moment$Moment | string | number | Date | Array,
- units?: ?string
- ): boolean,
- isBetween(
- fromDate: moment$Moment | string | number | Date | Array,
- toDate?: ?moment$Moment | string | number | Date | Array,
- granularity?: ?string,
- inclusion?: ?string
- ): boolean,
- isDST(): boolean,
- isDSTShifted(): boolean,
- isLeapYear(): boolean,
- clone(): moment$Moment,
- static isMoment(obj: any): boolean,
- static isDate(obj: any): boolean,
- static locale(locale: string, localeData?: Object): string,
- static updateLocale(locale: string, localeData?: ?Object): void,
- static locale(locales: Array): string,
- locale(locale: string, customization?: Object | null): moment$Moment,
- locale(): string,
- static months(): Array,
- static monthsShort(): Array,
- static weekdays(): Array,
- static weekdaysShort(): Array,
- static weekdaysMin(): Array,
- static months(): string,
- static monthsShort(): string,
- static weekdays(): string,
- static weekdaysShort(): string,
- static weekdaysMin(): string,
- static localeData(key?: string): moment$LocaleData,
- static duration(
- value: number | Object | string,
- unit?: string
- ): moment$MomentDuration,
- static isDuration(obj: any): boolean,
- static normalizeUnits(unit: string): string,
- static invalid(object: any): moment$Moment
-}
-
-declare module "moment" {
- declare module.exports: Class;
-}
diff --git a/scm-plugins/scm-git-plugin/flow-typed/npm/react-jss_vx.x.x.js b/scm-plugins/scm-git-plugin/flow-typed/npm/react-jss_vx.x.x.js
deleted file mode 100644
index cf8abae155..0000000000
--- a/scm-plugins/scm-git-plugin/flow-typed/npm/react-jss_vx.x.x.js
+++ /dev/null
@@ -1,137 +0,0 @@
-// flow-typed signature: ba35d02d668b0d0a3e04a63a6847974e
-// flow-typed version: <>/react-jss_v8.6.1/flow_v0.79.1
-
-/**
- * This is an autogenerated libdef stub for:
- *
- * 'react-jss'
- *
- * Fill this stub out by replacing all the `any` types.
- *
- * Once filled out, we encourage you to share your work with the
- * community by sending a pull request to:
- * https://github.com/flowtype/flow-typed
- */
-
-declare module 'react-jss' {
- declare module.exports: any;
-}
-
-/**
- * We include stubs for each file inside this npm package in case you need to
- * require those files directly. Feel free to delete any files that aren't
- * needed.
- */
-declare module 'react-jss/dist/react-jss' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/dist/react-jss.min' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/compose' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/compose.test' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/contextTypes' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/createHoc' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/getDisplayName' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/index' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/index.test' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/injectSheet' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/injectSheet.test' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/jss' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/JssProvider' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/JssProvider.test' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/ns' {
- declare module.exports: any;
-}
-
-declare module 'react-jss/lib/propTypes' {
- declare module.exports: any;
-}
-
-// Filename aliases
-declare module 'react-jss/dist/react-jss.js' {
- declare module.exports: $Exports<'react-jss/dist/react-jss'>;
-}
-declare module 'react-jss/dist/react-jss.min.js' {
- declare module.exports: $Exports<'react-jss/dist/react-jss.min'>;
-}
-declare module 'react-jss/lib/compose.js' {
- declare module.exports: $Exports<'react-jss/lib/compose'>;
-}
-declare module 'react-jss/lib/compose.test.js' {
- declare module.exports: $Exports<'react-jss/lib/compose.test'>;
-}
-declare module 'react-jss/lib/contextTypes.js' {
- declare module.exports: $Exports<'react-jss/lib/contextTypes'>;
-}
-declare module 'react-jss/lib/createHoc.js' {
- declare module.exports: $Exports<'react-jss/lib/createHoc'>;
-}
-declare module 'react-jss/lib/getDisplayName.js' {
- declare module.exports: $Exports<'react-jss/lib/getDisplayName'>;
-}
-declare module 'react-jss/lib/index.js' {
- declare module.exports: $Exports<'react-jss/lib/index'>;
-}
-declare module 'react-jss/lib/index.test.js' {
- declare module.exports: $Exports<'react-jss/lib/index.test'>;
-}
-declare module 'react-jss/lib/injectSheet.js' {
- declare module.exports: $Exports<'react-jss/lib/injectSheet'>;
-}
-declare module 'react-jss/lib/injectSheet.test.js' {
- declare module.exports: $Exports<'react-jss/lib/injectSheet.test'>;
-}
-declare module 'react-jss/lib/jss.js' {
- declare module.exports: $Exports<'react-jss/lib/jss'>;
-}
-declare module 'react-jss/lib/JssProvider.js' {
- declare module.exports: $Exports<'react-jss/lib/JssProvider'>;
-}
-declare module 'react-jss/lib/JssProvider.test.js' {
- declare module.exports: $Exports<'react-jss/lib/JssProvider.test'>;
-}
-declare module 'react-jss/lib/ns.js' {
- declare module.exports: $Exports<'react-jss/lib/ns'>;
-}
-declare module 'react-jss/lib/propTypes.js' {
- declare module.exports: $Exports<'react-jss/lib/propTypes'>;
-}
diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json
index 8f0ab90d34..6377574498 100644
--- a/scm-plugins/scm-git-plugin/package.json
+++ b/scm-plugins/scm-git-plugin/package.json
@@ -1,14 +1,17 @@
{
"name": "@scm-manager/scm-git-plugin",
- "license" : "BSD-3-Clause",
+ "license": "BSD-3-Clause",
"main": "src/main/js/index.js",
"scripts": {
- "build": "ui-bundler plugin"
+ "build": "ui-bundler plugin",
+ "watch": "ui-bundler plugin -w",
+ "lint": "ui-bundler lint",
+ "flow": "flow check"
},
"dependencies": {
- "@scm-manager/ui-extensions": "^0.0.7"
+ "@scm-manager/ui-extensions": "^0.1.1"
},
"devDependencies": {
- "@scm-manager/ui-bundler": "^0.0.17"
+ "@scm-manager/ui-bundler": "^0.0.21"
}
}
diff --git a/scm-plugins/scm-git-plugin/pom.xml b/scm-plugins/scm-git-plugin/pom.xml
index 11e2a40bd0..a838e2f146 100644
--- a/scm-plugins/scm-git-plugin/pom.xml
+++ b/scm-plugins/scm-git-plugin/pom.xml
@@ -43,11 +43,24 @@
-
-
+
+ sonia.scm.maven
+ smp-maven-plugin
+ true
+
+ true
+
+ @scm-manager/ui-types
+ @scm-manager/ui-components
+
+
+
+
+
+
org.apache.maven.plugins
maven-jar-plugin
@@ -61,33 +74,6 @@
-
- com.github.sdorra
- buildfrontend-maven-plugin
-
-
- link-ui-types
- process-sources
-
- install-link
-
-
- @scm-manager/ui-types
-
-
-
- link-ui-components
- process-sources
-
- install-link
-
-
- @scm-manager/ui-components
-
-
-
-
-
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java
deleted file mode 100644
index bace7d0b73..0000000000
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/rest/resources/GitConfigResource.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-package sonia.scm.api.rest.resources;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import sonia.scm.repository.GitConfig;
-import sonia.scm.repository.GitRepositoryHandler;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
-
-/**
- *
- * @author Sebastian Sdorra
- */
-@Singleton
-@Path("config/repositories/git")
-@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
-public class GitConfigResource
-{
-
- /**
- * Constructs ...
- *
- *
- *
- * @param repositoryHandler
- */
- @Inject
- public GitConfigResource(GitRepositoryHandler repositoryHandler)
- {
- this.repositoryHandler = repositoryHandler;
- }
-
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @return
- */
- @GET
- public GitConfig getConfig()
- {
- GitConfig config = repositoryHandler.getConfig();
-
- if (config == null)
- {
- config = new GitConfig();
- repositoryHandler.setConfig(config);
- }
-
- return config;
- }
-
- //~--- set methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param uriInfo
- * @param config
- *
- * @return
- */
- @POST
- @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
- public Response setConfig(@Context UriInfo uriInfo, GitConfig config)
- {
- repositoryHandler.setConfig(config);
- repositoryHandler.storeConfig();
-
- return Response.created(uriInfo.getRequestUri()).build();
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private GitRepositoryHandler repositoryHandler;
-}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java
new file mode 100644
index 0000000000..553f0f5a00
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java
@@ -0,0 +1,25 @@
+package sonia.scm.repository;
+
+import java.util.function.Consumer;
+
+public class CloseableWrapper implements AutoCloseable {
+
+ private final C wrapped;
+ private final Consumer cleanup;
+
+ public CloseableWrapper(C wrapped, Consumer cleanup) {
+ this.wrapped = wrapped;
+ this.cleanup = cleanup;
+ }
+
+ public C get() { return wrapped; }
+
+ @Override
+ public void close() {
+ try {
+ cleanup.accept(wrapped);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java
index f5c1857a89..d190eae567 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java
@@ -90,7 +90,9 @@ public class GitRepositoryHandler
private static final Object LOCK = new Object();
private final Scheduler scheduler;
-
+
+ private final GitWorkdirFactory workdirFactory;
+
private Task task;
//~--- constructors ---------------------------------------------------------
@@ -98,16 +100,18 @@ public class GitRepositoryHandler
/**
* Constructs ...
*
- * @param storeFactory
+ *
+ * @param storeFactory
* @param fileSystem
* @param scheduler
* @param repositoryLocationResolver
*/
@Inject
- public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, RepositoryLocationResolver repositoryLocationResolver)
+ public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, RepositoryLocationResolver repositoryLocationResolver, GitWorkdirFactory workdirFactory)
{
super(storeFactory, fileSystem, repositoryLocationResolver);
this.scheduler = scheduler;
+ this.workdirFactory = workdirFactory;
}
//~--- get methods ----------------------------------------------------------
@@ -116,17 +120,17 @@ public class GitRepositoryHandler
public void init(SCMContextProvider context)
{
super.init(context);
- scheduleGc();
+ scheduleGc(getConfig().getGcExpression());
}
@Override
public void setConfig(GitConfig config)
{
+ scheduleGc(config.getGcExpression());
super.setConfig(config);
- scheduleGc();
}
- private void scheduleGc()
+ private void scheduleGc(String expression)
{
synchronized (LOCK){
if ( task != null ){
@@ -134,11 +138,10 @@ public class GitRepositoryHandler
task.cancel();
task = null;
}
- String exp = getConfig().getGcExpression();
- if (!Strings.isNullOrEmpty(exp))
+ if (!Strings.isNullOrEmpty(expression))
{
- logger.info("schedule git gc task with expression {}", exp);
- task = scheduler.schedule(exp, GitGcTask.class);
+ logger.info("schedule git gc task with expression {}", expression);
+ task = scheduler.schedule(expression, GitGcTask.class);
}
}
}
@@ -235,4 +238,8 @@ public class GitRepositoryHandler
{
return new File(directory, DIRECTORY_REFS).exists();
}
+
+ public GitWorkdirFactory getWorkdirFactory() {
+ return workdirFactory;
+ }
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java
index 13340a20e7..f490b7ea4f 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java
@@ -48,6 +48,7 @@ import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
@@ -55,6 +56,7 @@ import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.ContextEntry;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import sonia.scm.web.GitUserAgentProvider;
@@ -203,7 +205,7 @@ public final class GitUtil
}
catch (GitAPIException ex)
{
- throw new InternalRepositoryException("could not fetch", ex);
+ throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("remote", directory.toString()).in(remoteRepository), "could not fetch", ex);
}
}
@@ -716,6 +718,18 @@ public final class GitUtil
return (id != null) &&!id.equals(ObjectId.zeroId());
}
+ /**
+ * Computes the first common ancestor of two revisions, aka merge base.
+ */
+ public static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
+ try (RevWalk mergeBaseWalk = new RevWalk(repository)) {
+ mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
+ mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1));
+ mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2));
+ return mergeBaseWalk.next().getId();
+ }
+ }
+
//~--- methods --------------------------------------------------------------
/**
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
new file mode 100644
index 0000000000..f93713a221
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java
@@ -0,0 +1,8 @@
+package sonia.scm.repository;
+
+import sonia.scm.repository.spi.GitContext;
+import sonia.scm.repository.spi.WorkingCopy;
+
+public interface GitWorkdirFactory {
+ WorkingCopy createWorkingCopy(GitContext gitContext);
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java
index e90a1c11ed..3cf72166ea 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitIncomingOutgoingCommand.java
@@ -160,7 +160,7 @@ public abstract class AbstractGitIncomingOutgoingCommand
}
catch (Exception ex)
{
- throw new InternalRepositoryException("could not execute incoming command", ex);
+ throw new InternalRepositoryException(repository, "could not execute incoming command", ex);
}
finally
{
@@ -200,13 +200,7 @@ public abstract class AbstractGitIncomingOutgoingCommand
{
if (e.getKey().startsWith(prefix))
{
- if (ref != null)
- {
- throw new InternalRepositoryException("could not find remote branch");
- }
-
ref = e.getValue();
-
break;
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java
index 75050c26ea..e4e37d6fed 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitPushOrPullCommand.java
@@ -114,7 +114,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand
}
catch (Exception ex)
{
- throw new InternalRepositoryException("could not execute push/pull command", ex);
+ throw new InternalRepositoryException(repository, "could not execute push/pull command", ex);
}
return counter;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java
index f50f245963..2ad38648da 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBlameCommand.java
@@ -55,6 +55,8 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+
//~--- JDK imports ------------------------------------------------------------
/**
@@ -108,9 +110,8 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand
if (gitBlameResult == null)
{
- throw new InternalRepositoryException(
- "could not create blame result for path ".concat(
- request.getPath()));
+ throw new InternalRepositoryException(entity("path", request.getPath()).in(repository),
+ "could not create blame result for path");
}
List blameLines = new ArrayList();
@@ -150,7 +151,7 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand
}
catch (GitAPIException ex)
{
- throw new InternalRepositoryException("could not create blame view", ex);
+ throw new InternalRepositoryException(repository, "could not create blame view", ex);
}
return result;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java
index 0cc47100de..4922752b6f 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java
@@ -102,7 +102,7 @@ public class GitBranchesCommand extends AbstractGitCommand
}
catch (GitAPIException ex)
{
- throw new InternalRepositoryException("could not read branches", ex);
+ throw new InternalRepositoryException(repository, "could not read branches", ex);
}
return branches;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
index ab1b0ae420..9186572858 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
@@ -55,9 +55,7 @@ import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitSubModuleParser;
import sonia.scm.repository.GitUtil;
-import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.Repository;
-import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.SubRepository;
import sonia.scm.util.Util;
@@ -104,7 +102,7 @@ public class GitBrowseCommand extends AbstractGitCommand
@Override
@SuppressWarnings("unchecked")
public BrowserResult getBrowserResult(BrowseCommandRequest request)
- throws IOException, NotFoundException {
+ throws IOException {
logger.debug("try to create browse result for {}", request);
BrowserResult result;
@@ -166,7 +164,7 @@ public class GitBrowseCommand extends AbstractGitCommand
*/
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
- throws IOException, RevisionNotFoundException {
+ throws IOException {
FileObject file = new FileObject();
@@ -258,7 +256,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return result;
}
- private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException, NotFoundException {
+ private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException {
RevWalk revWalk = null;
TreeWalk treeWalk = null;
@@ -309,7 +307,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath());
}
- private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException {
+ private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException {
List files = Lists.newArrayList();
while (treeWalk.next())
{
@@ -337,7 +335,7 @@ public class GitBrowseCommand extends AbstractGitCommand
}
private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo,
- BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException {
+ BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException {
String[] pathElements = request.getPath().split("/");
int currentDepth = 0;
int limit = pathElements.length;
@@ -363,7 +361,7 @@ public class GitBrowseCommand extends AbstractGitCommand
private Map getSubRepositories(org.eclipse.jgit.lib.Repository repo,
ObjectId revision)
- throws IOException, RevisionNotFoundException {
+ throws IOException {
if (logger.isDebugEnabled())
{
logger.debug("read submodules of {} at {}", repository.getName(),
@@ -377,7 +375,7 @@ public class GitBrowseCommand extends AbstractGitCommand
PATH_MODULES, baos);
subRepositories = GitSubModuleParser.parse(baos.toString());
}
- catch (PathNotFoundException ex)
+ catch (NotFoundException ex)
{
logger.trace("could not find .gitmodules", ex);
subRepositories = Collections.EMPTY_MAP;
@@ -388,7 +386,7 @@ public class GitBrowseCommand extends AbstractGitCommand
private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo,
ObjectId revId, String path)
- throws IOException, RevisionNotFoundException {
+ throws IOException {
Map subRepositories = subrepositoryCache.get(revId);
if (subRepositories == null)
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
index 4b05098d03..7477e0aee3 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
@@ -45,8 +45,6 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil;
-import sonia.scm.repository.PathNotFoundException;
-import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.util.Util;
import java.io.Closeable;
@@ -55,6 +53,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+import static sonia.scm.NotFoundException.notFound;
+
public class GitCatCommand extends AbstractGitCommand implements CatCommand {
@@ -65,7 +66,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
}
@Override
- public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException {
logger.debug("try to read content for {}", request);
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) {
closableObjectLoaderContainer.objectLoader.copyTo(output);
@@ -73,24 +74,24 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
}
@Override
- public InputStream getCatResultStream(CatCommandRequest request) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public InputStream getCatResultStream(CatCommandRequest request) throws IOException {
logger.debug("try to read content for {}", request);
return new InputStreamWrapper(getLoader(request));
}
- void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException {
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) {
closableObjectLoaderContainer.objectLoader.copyTo(output);
}
}
- private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException {
org.eclipse.jgit.lib.Repository repo = open();
ObjectId revId = getCommitOrDefault(repo, request.getRevision());
return getLoader(repo, revId, request.getPath());
}
- private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException {
TreeWalk treeWalk = new TreeWalk(repo);
treeWalk.setRecursive(Util.nonNull(path).contains("/"));
@@ -102,7 +103,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
try {
entry = revWalk.parseCommit(revId);
} catch (MissingObjectException e) {
- throw new RevisionNotFoundException(revId.getName());
+ throw notFound(entity("Revision", revId.getName()).in(repository));
}
RevTree revTree = entry.getTree();
@@ -120,7 +121,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk);
} else {
- throw new PathNotFoundException(path);
+ throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository));
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java
index 2175846d5a..b0dd8f1fd6 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java
@@ -38,6 +38,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil;
+import sonia.scm.repository.Repository;
//~--- JDK imports ------------------------------------------------------------
@@ -65,10 +66,12 @@ public class GitContext implements Closeable
*
*
* @param directory
+ * @param repository
*/
- public GitContext(File directory)
+ public GitContext(File directory, Repository repository)
{
this.directory = directory;
+ this.repository = repository;
}
//~--- methods --------------------------------------------------------------
@@ -82,8 +85,8 @@ public class GitContext implements Closeable
{
logger.trace("close git repository {}", directory);
- GitUtil.close(repository);
- repository = null;
+ GitUtil.close(gitRepository);
+ gitRepository = null;
}
/**
@@ -96,21 +99,30 @@ public class GitContext implements Closeable
*/
public org.eclipse.jgit.lib.Repository open() throws IOException
{
- if (repository == null)
+ if (gitRepository == null)
{
logger.trace("open git repository {}", directory);
- repository = GitUtil.open(directory);
+ gitRepository = GitUtil.open(directory);
}
+ return gitRepository;
+ }
+
+ Repository getRepository() {
return repository;
}
+ File getDirectory() {
+ return directory;
+ }
+
//~--- fields ---------------------------------------------------------------
/** Field description */
private final File directory;
+ private final Repository repository;
/** Field description */
- private org.eclipse.jgit.lib.Repository repository;
+ private org.eclipse.jgit.lib.Repository gitRepository;
}
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 83977ef290..2d56c8e786 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
@@ -34,27 +34,25 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
+import com.google.common.base.Strings;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.ObjectId;
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.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Repository;
import sonia.scm.util.Util;
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.BufferedOutputStream;
+import java.io.IOException;
import java.io.OutputStream;
-
import java.util.List;
/**
@@ -107,7 +105,8 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
walk = new RevWalk(gr);
- RevCommit commit = walk.parseCommit(gr.resolve(request.getRevision()));
+ ObjectId revision = gr.resolve(request.getRevision());
+ RevCommit commit = walk.parseCommit(revision);
walk.markStart(commit);
commit = walk.next();
@@ -120,7 +119,15 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
treeWalk.setFilter(PathFilter.create(request.getPath()));
}
- if (commit.getParentCount() > 0)
+
+ if (!Strings.isNullOrEmpty(request.getAncestorChangeset()))
+ {
+ ObjectId otherRevision = gr.resolve(request.getAncestorChangeset());
+ ObjectId ancestorId = computeCommonAncestor(gr, revision, otherRevision);
+ RevTree tree = walk.parseCommit(ancestorId).getTree();
+ treeWalk.addTree(tree);
+ }
+ else if (commit.getParentCount() > 0)
{
RevTree tree = commit.getParent(0).getTree();
@@ -156,7 +163,6 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
}
catch (Exception ex)
{
-
// TODO throw exception
logger.error("could not create diff", ex);
}
@@ -167,4 +173,9 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
GitUtil.release(formatter);
}
}
+
+ private ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
+ return GitUtil.computeCommonAncestor(repository, revision1, revision2);
+ }
+
}
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 4e9261f517..2ea25126cf 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
@@ -43,6 +43,7 @@ import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
@@ -53,7 +54,6 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
-import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.util.IOUtil;
import java.io.IOException;
@@ -61,6 +61,9 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+import static sonia.scm.NotFoundException.notFound;
+
//~--- JDK imports ------------------------------------------------------------
/**
@@ -85,7 +88,6 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
*
* @param context
* @param repository
- * @param repositoryDirectory
*/
GitLogCommand(GitContext context, sonia.scm.repository.Repository repository)
{
@@ -162,7 +164,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
*/
@Override
@SuppressWarnings("unchecked")
- public ChangesetPagingResult getChangesets(LogCommandRequest request) throws RevisionNotFoundException {
+ public ChangesetPagingResult getChangesets(LogCommandRequest request) {
if (logger.isDebugEnabled()) {
logger.debug("fetch changesets for request: {}", request);
}
@@ -198,6 +200,14 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
endId = repository.resolve(request.getEndChangeset());
}
+ Ref branch = getBranchOrDefault(repository,request.getBranch());
+
+ ObjectId ancestorId = null;
+
+ if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) {
+ ancestorId = computeCommonAncestor(request, repository, startId, branch);
+ }
+
revWalk = new RevWalk(repository);
converter = new GitChangesetConverter(repository, revWalk);
@@ -208,8 +218,6 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF));
}
- Ref branch = getBranchOrDefault(repository,request.getBranch());
-
if (branch != null) {
if (startId != null) {
revWalk.markStart(revWalk.lookupCommit(startId));
@@ -217,11 +225,16 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
}
+
Iterator iterator = revWalk.iterator();
while (iterator.hasNext()) {
RevCommit commit = iterator.next();
+ if (commit.getId().equals(ancestorId)) {
+ break;
+ }
+
if ((counter >= start)
&& ((limit < 0) || (counter < start + limit))) {
changesetList.add(converter.createChangeset(commit));
@@ -229,7 +242,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
counter++;
- if ((endId != null) && commit.getId().equals(endId)) {
+ if (commit.getId().equals(endId)) {
break;
}
}
@@ -249,11 +262,11 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
}
catch (MissingObjectException e)
{
- throw new RevisionNotFoundException(e.getObjectId().name());
+ throw notFound(entity("Revision", e.getObjectId().getName()).in(repository));
}
catch (Exception ex)
{
- throw new InternalRepositoryException("could not create change log", ex);
+ throw new InternalRepositoryException(repository, "could not create change log", ex);
}
finally
{
@@ -263,4 +276,17 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
return changesets;
}
+
+ private ObjectId computeCommonAncestor(LogCommandRequest request, Repository repository, ObjectId startId, Ref branch) throws IOException {
+ try (RevWalk mergeBaseWalk = new RevWalk(repository)) {
+ mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
+ if (startId != null) {
+ mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(startId));
+ } else {
+ mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(branch.getObjectId()));
+ }
+ mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(repository.resolve(request.getAncestorChangeset())));
+ return mergeBaseWalk.next().getId();
+ }
+ }
}
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
new file mode 100644
index 0000000000..5e9eac5230
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java
@@ -0,0 +1,169 @@
+package sonia.scm.repository.spi;
+
+import com.google.common.base.Strings;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.subject.Subject;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeResult;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
+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.user.User;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+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;
+
+ GitMergeCommand(GitContext context, sonia.scm.repository.Repository repository, GitWorkdirFactory workdirFactory) {
+ super(context, repository);
+ this.workdirFactory = workdirFactory;
+ }
+
+ @Override
+ public MergeCommandResult merge(MergeCommandRequest request) {
+ try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) {
+ Repository repository = workingCopy.get();
+ logger.debug("cloned repository to folder {}", repository.getWorkTree());
+ return new MergeWorker(repository, request).merge();
+ } catch (IOException e) {
+ throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e);
+ }
+ }
+
+ @Override
+ public MergeDryRunCommandResult dryRun(MergeCommandRequest request) {
+ try {
+ Repository repository = context.open();
+ ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
+ return new MergeDryRunCommandResult(merger.merge(repository.resolve(request.getBranchToMerge()), repository.resolve(request.getTargetBranch())));
+ } catch (IOException e) {
+ throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e);
+ }
+ }
+
+ private class MergeWorker {
+
+ private final String target;
+ private final String toMerge;
+ private final Person author;
+ private final Git clone;
+ private final String messageTemplate;
+
+ private MergeWorker(Repository clone, MergeCommandRequest request) {
+ this.target = request.getTargetBranch();
+ this.toMerge = request.getBranchToMerge();
+ this.author = request.getAuthor();
+ this.messageTemplate = request.getMessageTemplate();
+ this.clone = new Git(clone);
+ }
+
+ private MergeCommandResult merge() throws IOException {
+ checkOutTargetBranch();
+ MergeResult result = doMergeInClone();
+ if (result.getMergeStatus().isSuccessful()) {
+ doCommit();
+ push();
+ return MergeCommandResult.success();
+ } else {
+ return analyseFailure(result);
+ }
+ }
+
+ private void checkOutTargetBranch() {
+ try {
+ clone.checkout().setName(target).call();
+ } catch (GitAPIException e) {
+ throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge: " + target, e);
+ }
+ }
+
+ private MergeResult doMergeInClone() throws IOException {
+ MergeResult result;
+ try {
+ result = clone.merge()
+ .setCommit(false) // we want to set the author manually
+ .include(toMerge, resolveRevision(toMerge))
+ .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);
+ Person authorToUse = determineAuthor();
+ try {
+ clone.commit()
+ .setAuthor(authorToUse.getName(), authorToUse.getMail())
+ .setMessage(MessageFormat.format(determineMessageTemplate(), toMerge, target))
+ .call();
+ } catch (GitAPIException e) {
+ throw new InternalRepositoryException(context.getRepository(), "could not commit merge between branch " + toMerge + " and " + target, e);
+ }
+ }
+
+ private String determineMessageTemplate() {
+ if (Strings.isNullOrEmpty(messageTemplate)) {
+ return MERGE_COMMIT_MESSAGE_TEMPLATE;
+ } else {
+ return messageTemplate;
+ }
+ }
+
+ private Person determineAuthor() {
+ if (author == null) {
+ Subject subject = SecurityUtils.getSubject();
+ User user = subject.getPrincipals().oneByType(User.class);
+ String name = user.getDisplayName();
+ String email = user.getMail();
+ logger.debug("no author set; using logged in user: {} <{}>", name, email);
+ return new Person(name, email);
+ } else {
+ return author;
+ }
+ }
+
+ private void push() {
+ try {
+ clone.push().call();
+ } catch (GitAPIException e) {
+ throw new InternalRepositoryException(context.getRepository(), "could not push merged branch " + toMerge + " to origin", e);
+ }
+ logger.debug("pushed merged branch {}", target);
+ }
+
+ 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());
+ }
+
+ private ObjectId resolveRevision(String branchToMerge) throws IOException {
+ ObjectId resolved = clone.getRepository().resolve(branchToMerge);
+ if (resolved == null) {
+ return clone.getRepository().resolve("origin/" + branchToMerge);
+ } else {
+ return resolved;
+ }
+ }
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java
index 2b35ba74f6..5040069c12 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java
@@ -17,6 +17,8 @@ import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;
+import static sonia.scm.ContextEntry.ContextBuilder.entity;
+
@Slf4j
public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand {
@@ -26,7 +28,7 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
}
private Modifications createModifications(TreeWalk treeWalk, RevCommit commit, RevWalk revWalk, String revision)
- throws IOException, UnsupportedModificationTypeException {
+ throws IOException {
treeWalk.reset();
treeWalk.setRecursive(true);
if (commit.getParentCount() > 0) {
@@ -73,12 +75,7 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
}
} catch (IOException ex) {
log.error("could not open repository", ex);
- throw new InternalRepositoryException(ex);
-
- } catch (UnsupportedModificationTypeException ex) {
- log.error("Unsupported modification type", ex);
- throw new InternalRepositoryException(ex);
-
+ throw new InternalRepositoryException(entity(repository), "could not open repository", ex);
} finally {
GitUtil.release(revWalk);
GitUtil.close(gitRepository);
@@ -100,7 +97,7 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
} else if (type == DiffEntry.ChangeType.DELETE) {
modifications.getRemoved().add(entry.getOldPath());
} else {
- throw new UnsupportedModificationTypeException(MessageFormat.format("The modification type: {0} is not supported.", type));
+ throw new UnsupportedModificationTypeException(entity(repository), MessageFormat.format("The modification type: {0} is not supported.", type));
}
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
index 2a1f805cf6..6bdd8b793a 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitOutgoingCommand.java
@@ -77,7 +77,6 @@ public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand
* @return
*
* @throws IOException
- * @throws RepositoryException
*/
@Override
public ChangesetPagingResult getOutgoingChangesets(
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java
index a7b341ff5d..7a829355bf 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java
@@ -101,7 +101,6 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
* @return
*
* @throws IOException
- * @throws RepositoryException
*/
@Override
public PullResponse pull(PullCommandRequest request)
@@ -249,7 +248,7 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
}
catch (GitAPIException ex)
{
- throw new InternalRepositoryException("error durring pull", ex);
+ throw new InternalRepositoryException(repository, "error during pull", ex);
}
return response;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java
index 193d68d7a5..3963366aa7 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPushCommand.java
@@ -85,7 +85,6 @@ public class GitPushCommand extends AbstractGitPushOrPullCommand
* @return
*
* @throws IOException
- * @throws RepositoryException
*/
@Override
public PushResponse push(PushCommandRequest 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 d60abd424d..bda0d87b21 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
@@ -63,7 +63,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
Command.INCOMING,
Command.OUTGOING,
Command.PUSH,
- Command.PULL
+ Command.PULL,
+ Command.MERGE
);
//J+
@@ -72,7 +73,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository) {
this.handler = handler;
this.repository = repository;
- this.context = new GitContext(handler.getDirectory(repository));
+ this.context = new GitContext(handler.getDirectory(repository), repository);
}
//~--- methods --------------------------------------------------------------
@@ -240,7 +241,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitTagsCommand(context, repository);
}
- //~--- fields ---------------------------------------------------------------
+ @Override
+ public MergeCommand getMergeCommand() {
+ return new GitMergeCommand(context, repository, handler.getWorkdirFactory());
+ }
+
+//~--- fields ---------------------------------------------------------------
/** Field description */
private GitContext context;
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java
index 02fee3cef0..807ec807e6 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java
@@ -95,7 +95,7 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand
}
catch (GitAPIException ex)
{
- throw new InternalRepositoryException("could not read tags from repository", ex);
+ throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
}
finally
{
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
new file mode 100644
index 0000000000..22fce5f330
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java
@@ -0,0 +1,62 @@
+package sonia.scm.repository.spi;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.repository.GitWorkdirFactory;
+import sonia.scm.repository.InternalRepositoryException;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+public class SimpleGitWorkdirFactory implements GitWorkdirFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(SimpleGitWorkdirFactory.class);
+
+ private final File poolDirectory;
+
+ public SimpleGitWorkdirFactory() {
+ this(new File(System.getProperty("java.io.tmpdir"), "scmm-git-pool"));
+ }
+
+ public SimpleGitWorkdirFactory(File poolDirectory) {
+ this.poolDirectory = poolDirectory;
+ poolDirectory.mkdirs();
+ }
+
+ public WorkingCopy createWorkingCopy(GitContext gitContext) {
+ try {
+ Repository clone = cloneRepository(gitContext.getDirectory(), createNewWorkdir());
+ return new WorkingCopy(clone, this::close);
+ } catch (GitAPIException e) {
+ throw new InternalRepositoryException(gitContext.getRepository(), "could not clone working copy of repository", e);
+ } catch (IOException e) {
+ throw new InternalRepositoryException(gitContext.getRepository(), "could not create temporary directory for clone of repository", e);
+ }
+ }
+
+ private File createNewWorkdir() throws IOException {
+ return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
+ }
+
+ protected Repository cloneRepository(File bareRepository, File target) throws GitAPIException {
+ return Git.cloneRepository()
+ .setURI(bareRepository.getAbsolutePath())
+ .setDirectory(target)
+ .call()
+ .getRepository();
+ }
+
+ private void close(Repository repository) {
+ repository.close();
+ try {
+ FileUtils.delete(repository.getWorkTree(), FileUtils.RECURSIVE);
+ } catch (IOException e) {
+ logger.warn("could not delete temporary git workdir '{}'", repository.getWorkTree(), e);
+ }
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java
index 5081a29d21..85119a3e9f 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java
@@ -1,9 +1,10 @@
package sonia.scm.repository.spi;
+import sonia.scm.ContextEntry;
import sonia.scm.repository.InternalRepositoryException;
public class UnsupportedModificationTypeException extends InternalRepositoryException {
- public UnsupportedModificationTypeException(String message) {
- super(message);
+ public UnsupportedModificationTypeException(ContextEntry.ContextBuilder entity, String message) {
+ super(entity, message);
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java
new file mode 100644
index 0000000000..fd0cba510b
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java
@@ -0,0 +1,12 @@
+package sonia.scm.repository.spi;
+
+import org.eclipse.jgit.lib.Repository;
+import sonia.scm.repository.CloseableWrapper;
+
+import java.util.function.Consumer;
+
+public class WorkingCopy extends CloseableWrapper {
+ WorkingCopy(Repository wrapped, Consumer cleanup) {
+ super(wrapped, cleanup);
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryViewer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryViewer.java
index 773e09aada..82e19c77b7 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryViewer.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryViewer.java
@@ -151,7 +151,6 @@ public class GitRepositoryViewer
* @return
*
* @throws IOException
- * @throws RepositoryException
*/
private BranchesModel createBranchesModel(Repository repository)
throws IOException
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java
index e731e01a62..a3dac0e7d1 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java
@@ -41,6 +41,8 @@ import org.mapstruct.factory.Mappers;
import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper;
import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper;
import sonia.scm.plugin.Extension;
+import sonia.scm.repository.GitWorkdirFactory;
+import sonia.scm.repository.spi.SimpleGitWorkdirFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
@@ -63,5 +65,7 @@ public class GitServletModule extends ServletModule
bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
+
+ bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class);
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js
new file mode 100644
index 0000000000..630984ad87
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js
@@ -0,0 +1,79 @@
+//@flow
+import React from "react";
+import { translate } from "react-i18next";
+import type { Links } from "@scm-manager/ui-types";
+
+import { InputField, Checkbox } from "@scm-manager/ui-components";
+
+type Configuration = {
+ repositoryDirectory?: string,
+ gcExpression?: string,
+ disabled: boolean,
+ _links: Links
+}
+
+type Props = {
+ initialConfiguration: Configuration,
+ readOnly: boolean,
+
+ onConfigurationChange: (Configuration, boolean) => void,
+
+ // context props
+ t: (string) => string
+}
+
+type State = Configuration & {
+
+}
+
+class GitConfigurationForm extends React.Component {
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { ...props.initialConfiguration };
+ }
+
+ isValid = () => {
+ return !!this.state.repositoryDirectory;
+ };
+
+ handleChange = (value: any, name: string) => {
+ this.setState({
+ [name]: value
+ }, () => this.props.onConfigurationChange(this.state, this.isValid()));
+ };
+
+ render() {
+ const { repositoryDirectory, gcExpression, disabled } = this.state;
+ const { readOnly, t } = this.props;
+
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+
+}
+
+export default translate("plugins")(GitConfigurationForm);
diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.js b/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.js
new file mode 100644
index 0000000000..3718cc2900
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.js
@@ -0,0 +1,32 @@
+//@flow
+import React from "react";
+import { translate } from "react-i18next";
+import { Title, GlobalConfiguration } from "@scm-manager/ui-components";
+import GitConfigurationForm from "./GitConfigurationForm";
+
+type Props = {
+ link: string,
+
+ t: (string) => string
+};
+
+class GitGlobalConfiguration extends React.Component {
+
+ constructor(props: Props) {
+ super(props);
+ }
+
+ render() {
+ const { link, t } = this.props;
+
+ return (
+
+
+ }/>
+
+ );
+ }
+
+}
+
+export default translate("plugins")(GitGlobalConfiguration);
diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.js b/scm-plugins/scm-git-plugin/src/main/js/index.js
index be40135f8d..3f91405509 100644
--- a/scm-plugins/scm-git-plugin/src/main/js/index.js
+++ b/scm-plugins/scm-git-plugin/src/main/js/index.js
@@ -3,9 +3,18 @@ import { binder } from "@scm-manager/ui-extensions";
import ProtocolInformation from "./ProtocolInformation";
import GitAvatar from "./GitAvatar";
+import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
+import GitGlobalConfiguration from "./GitGlobalConfiguration";
+
+// repository
+
const gitPredicate = (props: Object) => {
return props.repository && props.repository.type === "git";
};
binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate);
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
+
+// global config
+
+cfgBinder.bindGlobal("/git", "scm-git-plugin.config.link", "gitConfig", GitGlobalConfiguration);
diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json
index 65594bae19..8cb801ac2c 100644
--- a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json
+++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json
@@ -4,6 +4,17 @@
"clone" : "Clone the repository",
"create" : "Create a new repository",
"replace" : "Push an existing repository"
+ },
+ "config": {
+ "link": "Git",
+ "title": "Git Configuration",
+ "directory": "Repository Directory",
+ "directoryHelpText": "Location of the Git repositories.",
+ "gcExpression": "GC Cron Expression",
+ "gcExpressionHelpText": "Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.",
+ "disabled": "Disabled",
+ "disabledHelpText": "Enable or disable the Git plugin",
+ "submit": "Submit"
}
}
}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java
new file mode 100644
index 0000000000..e92ee7abb5
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java
@@ -0,0 +1,29 @@
+package sonia.scm.repository;
+
+import org.junit.Test;
+
+import java.util.function.Consumer;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+public class CloseableWrapperTest {
+
+ @Test
+ public void shouldExecuteGivenMethodAtClose() {
+ Consumer wrapped = new Consumer() {
+ // no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy
+ @Override
+ public void accept(String s) {
+ }
+ };
+
+ Consumer closer = spy(wrapped);
+
+ try (CloseableWrapper wrapper = new CloseableWrapper<>("test", closer)) {
+ // nothing to do here
+ }
+
+ verify(closer).accept("test");
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java
index 90627f98e3..ace6756cad 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java
@@ -65,6 +65,9 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock
private ConfigurationStoreFactory factory;
+ @Mock
+ private GitWorkdirFactory gitWorkdirFactory;
+
RepositoryLocationResolver repositoryLocationResolver ;
private Path repoDir;
@@ -95,7 +98,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(contextProvider,fileSystem);
repositoryLocationResolver = new RepositoryLocationResolver(repoDao, initialRepositoryLocationResolver);
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
- fileSystem, scheduler, repositoryLocationResolver);
+ fileSystem, scheduler, repositoryLocationResolver, gitWorkdirFactory);
repoDir = directory.toPath();
when(repoDao.getPath(any())).thenReturn(repoDir);
@@ -112,7 +115,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
- new DefaultFileSystem(), scheduler, repositoryLocationResolver);
+ new DefaultFileSystem(), scheduler, repositoryLocationResolver, gitWorkdirFactory);
Repository repository = new Repository("id", "git", "Space", "Name");
GitConfig config = new GitConfig();
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java
index 496b71e656..0b3c1d6e9d 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java
@@ -50,7 +50,9 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
@After
public void close()
{
- context.close();
+ if (context != null) {
+ context.close();
+ }
}
/**
@@ -63,7 +65,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
{
if (context == null)
{
- context = new GitContext(repositoryDirectory);
+ context = new GitContext(repositoryDirectory, repository);
}
return context;
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java
index d0fd627046..5757cd5d5e 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBlameCommandTest.java
@@ -85,7 +85,6 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase
*
*
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testGetBlameResult() throws IOException
@@ -119,7 +118,6 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase
*
*
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testGetBlameResultWithRevision()
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
index 92b7ff69a9..5e63adfb70 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
@@ -32,7 +32,6 @@
package sonia.scm.repository.spi;
import org.junit.Test;
-import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitConstants;
@@ -54,7 +53,7 @@ import static org.junit.Assert.assertTrue;
public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
@Test
- public void testGetFile() throws IOException, NotFoundException {
+ public void testDefaultBranch() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("a.txt");
BrowserResult result = createCommand().getBrowserResult(request);
@@ -63,7 +62,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
}
@Test
- public void testDefaultDefaultBranch() throws IOException, NotFoundException {
+ public void testDefaultDefaultBranch() throws IOException {
// without default branch, the repository head should be used
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
assertNotNull(root);
@@ -78,7 +77,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
}
@Test
- public void testExplicitDefaultBranch() throws IOException, NotFoundException {
+ public void testExplicitDefaultBranch() throws IOException {
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
@@ -91,7 +90,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
}
@Test
- public void testBrowse() throws IOException, NotFoundException {
+ public void testBrowse() throws IOException {
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
assertNotNull(root);
@@ -113,7 +112,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
}
@Test
- public void testBrowseSubDirectory() throws IOException, NotFoundException {
+ public void testBrowseSubDirectory() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("c");
@@ -143,7 +142,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
}
@Test
- public void testRecursive() throws IOException, NotFoundException {
+ public void testRecusive() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setRecursive(true);
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java
index 3611c9c636..079fcac1da 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java
@@ -32,10 +32,13 @@
package sonia.scm.repository.spi;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import sonia.scm.NotFoundException;
import sonia.scm.repository.GitConstants;
-import sonia.scm.repository.PathNotFoundException;
-import sonia.scm.repository.RevisionNotFoundException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -51,9 +54,12 @@ import static org.junit.Assert.assertEquals;
* @author Sebastian Sdorra
*/
public class GitCatCommandTest extends AbstractGitCommandTestBase {
-
+
+ @Rule
+ public final ExpectedException expectedException = ExpectedException.none();
+
@Test
- public void testDefaultBranch() throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public void testDefaultBranch() throws IOException {
// without default branch, the repository head should be used
CatCommandRequest request = new CatCommandRequest();
request.setPath("a.txt");
@@ -66,7 +72,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
}
@Test
- public void testCat() throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public void testCat() throws IOException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("a.txt");
@@ -75,32 +81,58 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
}
@Test
- public void testSimpleCat() throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public void testSimpleCat() throws IOException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("b.txt");
assertEquals("b", execute(request));
}
- @Test(expected = PathNotFoundException.class)
- public void testUnknownFile() throws IOException, PathNotFoundException, RevisionNotFoundException {
+ @Test
+ public void testUnknownFile() throws IOException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("unknown");
- execute(request);
- }
- @Test(expected = RevisionNotFoundException.class)
- public void testUnknownRevision() throws IOException, PathNotFoundException, RevisionNotFoundException {
- CatCommandRequest request = new CatCommandRequest();
+ expectedException.expect(new BaseMatcher() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("expected NotFoundException for path");
+ }
+
+ @Override
+ public boolean matches(Object item) {
+ return "Path".equals(((NotFoundException)item).getContext().get(0).getType());
+ }
+ });
- request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
- request.setPath("a.txt");
execute(request);
}
@Test
- public void testSimpleStream() throws IOException, PathNotFoundException, RevisionNotFoundException {
+ public void testUnknownRevision() throws IOException {
+ CatCommandRequest request = new CatCommandRequest();
+
+ request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ request.setPath("a.txt");
+
+ expectedException.expect(new BaseMatcher() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("expected NotFoundException for revision");
+ }
+
+ @Override
+ public boolean matches(Object item) {
+ return "Revision".equals(((NotFoundException)item).getContext().get(0).getType());
+ }
+ });
+
+ execute(request);
+ }
+
+ @Test
+ public void testSimpleStream() throws IOException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("b.txt");
@@ -113,7 +145,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
catResultStream.close();
}
- private String execute(CatCommandRequest request) throws IOException, PathNotFoundException, RevisionNotFoundException {
+ private String execute(CatCommandRequest request) throws IOException {
String content = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
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
new file mode 100644
index 0000000000..f6e462f968
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java
@@ -0,0 +1,93 @@
+package sonia.scm.repository.spi;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+
+import static org.junit.Assert.assertEquals;
+
+public class GitDiffCommandTest extends AbstractGitCommandTestBase {
+
+ public static final String DIFF_FILE_A = "diff --git a/a.txt b/a.txt\n" +
+ "index 7898192..1dc60c7 100644\n" +
+ "--- a/a.txt\n" +
+ "+++ b/a.txt\n" +
+ "@@ -1 +1 @@\n" +
+ "-a\n" +
+ "+a and b\n";
+ public static final String DIFF_FILE_B = "diff --git a/b.txt b/b.txt\n" +
+ "deleted file mode 100644\n" +
+ "index 6178079..0000000\n" +
+ "--- a/b.txt\n" +
+ "+++ /dev/null\n" +
+ "@@ -1 +0,0 @@\n" +
+ "-b\n";
+ public static final String DIFF_FILE_A_MULTIPLE_REVISIONS = "diff --git a/a.txt b/a.txt\n" +
+ "index 7898192..2f8bc28 100644\n" +
+ "--- a/a.txt\n" +
+ "+++ b/a.txt\n" +
+ "@@ -1 +1,2 @@\n" +
+ " a\n" +
+ "+line for blame\n";
+ public static final String DIFF_FILE_F_MULTIPLE_REVISIONS = "diff --git a/f.txt b/f.txt\n" +
+ "new file mode 100644\n" +
+ "index 0000000..6a69f92\n" +
+ "--- /dev/null\n" +
+ "+++ b/f.txt\n" +
+ "@@ -0,0 +1 @@\n" +
+ "+f\n";
+
+ @Test
+ public void diffForOneRevisionShouldCreateDiff() {
+ GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
+ DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
+ diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ gitDiffCommand.getDiffResult(diffCommandRequest, output);
+ assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
+ }
+
+ @Test
+ public void diffForOneBranchShouldCreateDiff() {
+ GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
+ DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
+ diffCommandRequest.setRevision("test-branch");
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ gitDiffCommand.getDiffResult(diffCommandRequest, output);
+ assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
+ }
+
+ @Test
+ public void diffForPathShouldCreateLimitedDiff() {
+ GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
+ DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
+ diffCommandRequest.setRevision("test-branch");
+ diffCommandRequest.setPath("a.txt");
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ gitDiffCommand.getDiffResult(diffCommandRequest, output);
+ assertEquals(DIFF_FILE_A, output.toString());
+ }
+
+ @Test
+ public void diffBetweenTwoBranchesShouldCreateDiff() {
+ GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
+ DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
+ diffCommandRequest.setRevision("master");
+ diffCommandRequest.setAncestorChangeset("test-branch");
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ gitDiffCommand.getDiffResult(diffCommandRequest, output);
+ assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS + DIFF_FILE_F_MULTIPLE_REVISIONS, output.toString());
+ }
+
+ @Test
+ public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() {
+ GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository);
+ DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
+ diffCommandRequest.setRevision("master");
+ diffCommandRequest.setAncestorChangeset("test-branch");
+ diffCommandRequest.setPath("a.txt");
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ gitDiffCommand.getDiffResult(diffCommandRequest, output);
+ assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString());
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java
index e3d36601e7..acf0b0f820 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java
@@ -61,7 +61,6 @@ public class GitIncomingCommandTest
*
* @throws GitAPIException
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testGetIncomingChangesets()
@@ -95,7 +94,6 @@ public class GitIncomingCommandTest
*
* @throws GitAPIException
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testGetIncomingChangesetsWithAllreadyPullChangesets()
@@ -105,7 +103,7 @@ public class GitIncomingCommandTest
commit(outgoing, "added a");
- GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomingRepository);
+ GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, null), incomingRepository);
PullCommandRequest req = new PullCommandRequest();
req.setRemoteRepository(outgoingRepository);
pull.pull(req);
@@ -132,7 +130,6 @@ public class GitIncomingCommandTest
*
*
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testGetIncomingChangesetsWithEmptyRepository()
@@ -156,7 +153,6 @@ public class GitIncomingCommandTest
*
* @throws GitAPIException
* @throws IOException
- * @throws RepositoryException
*/
@Test
@Ignore
@@ -191,7 +187,7 @@ public class GitIncomingCommandTest
*/
private GitIncomingCommand createCommand()
{
- return new GitIncomingCommand(handler, new GitContext(incomingDirectory),
+ return new GitIncomingCommand(handler, new GitContext(incomingDirectory, null),
incomingRepository);
}
}
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 78db8ae686..4afaf09c67 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
@@ -64,7 +64,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
* Tests log command with the usage of a default branch.
*/
@Test
- public void testGetDefaultBranch() throws Exception {
+ public void testGetDefaultBranch() {
// without default branch, the repository head should be used
ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest());
@@ -92,7 +92,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
}
@Test
- public void testGetAll() throws Exception
+ public void testGetAll()
{
ChangesetPagingResult result =
createCommand().getChangesets(new LogCommandRequest());
@@ -103,7 +103,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
}
@Test
- public void testGetAllByPath() throws Exception
+ public void testGetAllByPath()
{
LogCommandRequest request = new LogCommandRequest();
@@ -119,7 +119,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
}
@Test
- public void testGetAllWithLimit() throws Exception
+ public void testGetAllWithLimit()
{
LogCommandRequest request = new LogCommandRequest();
@@ -143,7 +143,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
}
@Test
- public void testGetAllWithPaging() throws Exception
+ public void testGetAllWithPaging()
{
LogCommandRequest request = new LogCommandRequest();
@@ -194,7 +194,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
}
@Test
- public void testGetRange() throws Exception
+ public void testGetRange()
{
LogCommandRequest request = new LogCommandRequest();
@@ -216,6 +216,26 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId());
}
+ @Test
+ public void testGetAncestor()
+ {
+ LogCommandRequest request = new LogCommandRequest();
+
+ request.setBranch("test-branch");
+ request.setAncestorChangeset("master");
+
+ ChangesetPagingResult result = createCommand().getChangesets(request);
+
+ assertNotNull(result);
+ assertEquals(1, result.getTotal());
+ assertEquals(1, result.getChangesets().size());
+
+ Changeset c = result.getChangesets().get(0);
+
+ assertNotNull(c);
+ assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", c.getId());
+ }
+
@Test
public void shouldFindDefaultBranchFromHEAD() throws Exception {
setRepositoryHeadReference("ref: refs/heads/test-branch");
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
new file mode 100644
index 0000000000..1fca7814ed
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java
@@ -0,0 +1,139 @@
+package sonia.scm.repository.spi;
+
+import com.github.sdorra.shiro.ShiroRule;
+import com.github.sdorra.shiro.SubjectAware;
+import org.apache.shiro.subject.SimplePrincipalCollection;
+import org.apache.shiro.subject.Subject;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Rule;
+import org.junit.Test;
+import sonia.scm.repository.Person;
+import sonia.scm.repository.api.MergeCommandResult;
+import sonia.scm.user.User;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
+public class GitMergeCommandTest extends AbstractGitCommandTestBase {
+
+ private static final String REALM = "AdminRealm";
+
+ @Rule
+ public ShiroRule shiro = new ShiroRule();
+
+ @Test
+ public void shouldDetectMergeableBranches() {
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setBranchToMerge("mergeable");
+ request.setTargetBranch("master");
+
+ boolean mergeable = command.dryRun(request).isMergeable();
+
+ assertThat(mergeable).isTrue();
+ }
+
+ @Test
+ public void shouldDetectNotMergeableBranches() {
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setBranchToMerge("test-branch");
+ request.setTargetBranch("master");
+
+ boolean mergeable = command.dryRun(request).isMergeable();
+
+ assertThat(mergeable).isFalse();
+ }
+
+ @Test
+ public void shouldMergeMergeableBranches() throws IOException, GitAPIException {
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setTargetBranch("master");
+ request.setBranchToMerge("mergeable");
+ 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();
+ String message = mergeCommit.getFullMessage();
+ assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
+ assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
+ assertThat(message).contains("master", "mergeable");
+ // We expect the merge result of file b.txt here by looking up the sha hash of its content.
+ // If the file is missing (aka not merged correctly) this will throw a MissingObjectException:
+ byte[] contentOfFileB = repository.open(repository.resolve("9513e9c76e73f3e562fd8e4c909d0607113c77c6")).getBytes();
+ assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
+ }
+
+ @Test
+ public void shouldUseConfiguredCommitMessageTemplate() throws IOException, GitAPIException {
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setTargetBranch("master");
+ request.setBranchToMerge("mergeable");
+ request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
+ request.setMessageTemplate("simple");
+
+ 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();
+ String message = mergeCommit.getFullMessage();
+ assertThat(message).isEqualTo("simple");
+ }
+
+ @Test
+ public void shouldNotMergeConflictingBranches() {
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setBranchToMerge("test-branch");
+ request.setTargetBranch("master");
+
+ MergeCommandResult mergeCommandResult = command.merge(request);
+
+ assertThat(mergeCommandResult.isSuccess()).isFalse();
+ assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt");
+ }
+
+ @Test
+ @SubjectAware(username = "admin", password = "secret")
+ public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException {
+ shiro.setSubject(
+ new Subject.Builder()
+ .principals(new SimplePrincipalCollection(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM))
+ .buildSubject());
+ GitMergeCommand command = createCommand();
+ MergeCommandRequest request = new MergeCommandRequest();
+ request.setTargetBranch("master");
+ request.setBranchToMerge("mergeable");
+
+ MergeCommandResult mergeCommandResult = command.merge(request);
+
+ assertThat(mergeCommandResult.isSuccess()).isTrue();
+
+ Repository repository = createContext().open();
+ Iterable mergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
+ PersonIdent mergeAuthor = mergeCommit.iterator().next().getAuthorIdent();
+ assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
+ assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
+ }
+
+ private GitMergeCommand createCommand() {
+ return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory());
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java
index fb982f6f0c..41a516a124 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java
@@ -18,8 +18,8 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
@Before
public void init() {
- incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory), incomingRepository);
- outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory), outgoingRepository);
+ incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, null), incomingRepository);
+ outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, null), outgoingRepository);
}
@Test
@@ -63,12 +63,12 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
}
void pushOutgoingAndPullIncoming() throws IOException {
- GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory),
+ GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository);
PushCommandRequest request = new PushCommandRequest();
request.setRemoteRepository(incomingRepository);
cmd.push(request);
- GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory),
+ GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, null),
incomingRepository);
PullCommandRequest pullRequest = new PullCommandRequest();
pullRequest.setRemoteRepository(incomingRepository);
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java
index 3510650fb4..65592cf7e4 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java
@@ -61,7 +61,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*
* @throws GitAPIException
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testGetOutgoingChangesets()
@@ -95,7 +94,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*
* @throws GitAPIException
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testGetOutgoingChangesetsWithAlreadyPushedChanges()
@@ -106,7 +104,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
commit(outgoing, "added a");
GitPushCommand push = new GitPushCommand(handler,
- new GitContext(outgoingDirectory),
+ new GitContext(outgoingDirectory, null),
outgoingRepository);
PushCommandRequest req = new PushCommandRequest();
@@ -135,7 +133,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*
*
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testGetOutgoingChangesetsWithEmptyRepository()
@@ -161,7 +158,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*/
private GitOutgoingCommand createCommand()
{
- return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory),
+ return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository);
}
}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java
index 4f3d7e933d..70212ba233 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java
@@ -61,7 +61,6 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
*
* @throws GitAPIException
* @throws IOException
- * @throws RepositoryException
*/
@Test
public void testPush()
@@ -99,7 +98,7 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
*/
private GitPushCommand createCommand()
{
- return new GitPushCommand(handler, new GitContext(outgoingDirectory),
+ return new GitPushCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository);
}
}
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
new file mode 100644
index 0000000000..0c39a1deb0
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java
@@ -0,0 +1,87 @@
+package sonia.scm.repository.spi;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void emptyPoolShouldCreateNewWorkdir() throws IOException {
+ SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
+ File masterRepo = createRepositoryDirectory();
+
+ try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
+
+ assertThat(workingCopy.get().getDirectory())
+ .exists()
+ .isNotEqualTo(masterRepo)
+ .isDirectory();
+ assertThat(new File(workingCopy.get().getWorkTree(), "a.txt"))
+ .exists()
+ .isFile()
+ .hasContent("a\nline for blame");
+ }
+ }
+
+ @Test
+ public void cloneFromPoolShouldBeClosed() throws IOException {
+ PoolWithSpy factory = new PoolWithSpy(temporaryFolder.newFolder());
+
+ try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
+ assertThat(workingCopy).isNotNull();
+ }
+ verify(factory.createdClone).close();
+ }
+
+ @Test
+ public void cloneFromPoolShouldNotBeReused() throws IOException {
+ SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
+
+ File firstDirectory;
+ try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
+ firstDirectory = workingCopy.get().getDirectory();
+ }
+ try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
+ File secondDirectory = workingCopy.get().getDirectory();
+ assertThat(secondDirectory).isNotEqualTo(firstDirectory);
+ }
+ }
+
+ @Test
+ public void cloneFromPoolShouldBeDeletedOnClose() throws IOException {
+ SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
+
+ File directory;
+ try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
+ directory = workingCopy.get().getWorkTree();
+ }
+ assertThat(directory).doesNotExist();
+ }
+
+ private static class PoolWithSpy extends SimpleGitWorkdirFactory {
+ PoolWithSpy(File poolDirectory) {
+ super(poolDirectory);
+ }
+
+ Repository createdClone;
+
+ @Override
+ protected Repository cloneRepository(File bareRepository, File destination) throws GitAPIException {
+ createdClone = spy(super.cloneRepository(bareRepository, destination));
+ return createdClone;
+ }
+ }
+}
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 3fbab0be38..8f689e9664 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-git-plugin/yarn.lock b/scm-plugins/scm-git-plugin/yarn.lock
index c1abfd6640..3514ed3f2c 100644
--- a/scm-plugins/scm-git-plugin/yarn.lock
+++ b/scm-plugins/scm-git-plugin/yarn.lock
@@ -707,9 +707,9 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
-"@scm-manager/ui-bundler@^0.0.17":
- version "0.0.17"
- resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
+"@scm-manager/ui-bundler@^0.0.21":
+ version "0.0.21"
+ resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -747,9 +747,9 @@
vinyl-source-stream "^2.0.0"
watchify "^3.11.0"
-"@scm-manager/ui-extensions@^0.0.7":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e"
+"@scm-manager/ui-extensions@^0.1.1":
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45"
dependencies:
react "^16.4.2"
react-dom "^16.4.2"
diff --git a/scm-plugins/scm-hg-plugin/.flowconfig b/scm-plugins/scm-hg-plugin/.flowconfig
index 7ede008602..b05e157358 100644
--- a/scm-plugins/scm-hg-plugin/.flowconfig
+++ b/scm-plugins/scm-hg-plugin/.flowconfig
@@ -4,5 +4,6 @@
[include]
[libs]
+./node_modules/@scm-manager/ui-components/flow-typed
[options]
diff --git a/scm-plugins/scm-hg-plugin/flow-typed/npm/classnames_v2.x.x.js b/scm-plugins/scm-hg-plugin/flow-typed/npm/classnames_v2.x.x.js
deleted file mode 100644
index 2307243eeb..0000000000
--- a/scm-plugins/scm-hg-plugin/flow-typed/npm/classnames_v2.x.x.js
+++ /dev/null
@@ -1,23 +0,0 @@
-// flow-typed signature: cf86673cc32d185bdab1d2ea90578d37
-// flow-typed version: 614bf49aa8/classnames_v2.x.x/flow_>=v0.25.x
-
-type $npm$classnames$Classes =
- | string
- | { [className: string]: * }
- | false
- | void
- | null;
-
-declare module "classnames" {
- declare module.exports: (
- ...classes: Array<$npm$classnames$Classes | $npm$classnames$Classes[]>
- ) => string;
-}
-
-declare module "classnames/bind" {
- declare module.exports: $Exports<"classnames">;
-}
-
-declare module "classnames/dedupe" {
- declare module.exports: $Exports<"classnames">;
-}
diff --git a/scm-plugins/scm-hg-plugin/flow-typed/npm/jest_v23.x.x.js b/scm-plugins/scm-hg-plugin/flow-typed/npm/jest_v23.x.x.js
deleted file mode 100644
index 23b66b07e5..0000000000
--- a/scm-plugins/scm-hg-plugin/flow-typed/npm/jest_v23.x.x.js
+++ /dev/null
@@ -1,1108 +0,0 @@
-// flow-typed signature: f5a484315a3dea13d273645306e4076a
-// flow-typed version: 7c5d14b3d4/jest_v23.x.x/flow_>=v0.39.x
-
-type JestMockFn, TReturn> = {
- (...args: TArguments): TReturn,
- /**
- * An object for introspecting mock calls
- */
- mock: {
- /**
- * An array that represents all calls that have been made into this mock
- * function. Each call is represented by an array of arguments that were
- * passed during the call.
- */
- calls: Array,
- /**
- * An array that contains all the object instances that have been
- * instantiated from this mock function.
- */
- instances: Array
- },
- /**
- * Resets all information stored in the mockFn.mock.calls and
- * mockFn.mock.instances arrays. Often this is useful when you want to clean
- * up a mock's usage data between two assertions.
- */
- mockClear(): void,
- /**
- * Resets all information stored in the mock. This is useful when you want to
- * completely restore a mock back to its initial state.
- */
- mockReset(): void,
- /**
- * Removes the mock and restores the initial implementation. This is useful
- * when you want to mock functions in certain test cases and restore the
- * original implementation in others. Beware that mockFn.mockRestore only
- * works when mock was created with jest.spyOn. Thus you have to take care of
- * restoration yourself when manually assigning jest.fn().
- */
- mockRestore(): void,
- /**
- * Accepts a function that should be used as the implementation of the mock.
- * The mock itself will still record all calls that go into and instances
- * that come from itself -- the only difference is that the implementation
- * will also be executed when the mock is called.
- */
- mockImplementation(
- fn: (...args: TArguments) => TReturn
- ): JestMockFn,
- /**
- * Accepts a function that will be used as an implementation of the mock for
- * one call to the mocked function. Can be chained so that multiple function
- * calls produce different results.
- */
- mockImplementationOnce(
- fn: (...args: TArguments) => TReturn
- ): JestMockFn,
- /**
- * Accepts a string to use in test result output in place of "jest.fn()" to
- * indicate which mock function is being referenced.
- */
- mockName(name: string): JestMockFn,
- /**
- * Just a simple sugar function for returning `this`
- */
- mockReturnThis(): void,
- /**
- * Accepts a value that will be returned whenever the mock function is called.
- */
- mockReturnValue(value: TReturn): JestMockFn,
- /**
- * Sugar for only returning a value once inside your mock
- */
- mockReturnValueOnce(value: TReturn): JestMockFn,
- /**
- * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value))
- */
- mockResolvedValue(value: TReturn): JestMockFn>,
- /**
- * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value))
- */
- mockResolvedValueOnce(value: TReturn): JestMockFn>,
- /**
- * Sugar for jest.fn().mockImplementation(() => Promise.reject(value))
- */
- mockRejectedValue(value: TReturn): JestMockFn>,
- /**
- * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value))
- */
- mockRejectedValueOnce(value: TReturn): JestMockFn>
-};
-
-type JestAsymmetricEqualityType = {
- /**
- * A custom Jasmine equality tester
- */
- asymmetricMatch(value: mixed): boolean
-};
-
-type JestCallsType = {
- allArgs(): mixed,
- all(): mixed,
- any(): boolean,
- count(): number,
- first(): mixed,
- mostRecent(): mixed,
- reset(): void
-};
-
-type JestClockType = {
- install(): void,
- mockDate(date: Date): void,
- tick(milliseconds?: number): void,
- uninstall(): void
-};
-
-type JestMatcherResult = {
- message?: string | (() => string),
- pass: boolean
-};
-
-type JestMatcher = (actual: any, expected: any) => JestMatcherResult;
-
-type JestPromiseType = {
- /**
- * Use rejects to unwrap the reason of a rejected promise so any other
- * matcher can be chained. If the promise is fulfilled the assertion fails.
- */
- rejects: JestExpectType,
- /**
- * Use resolves to unwrap the value of a fulfilled promise so any other
- * matcher can be chained. If the promise is rejected the assertion fails.
- */
- resolves: JestExpectType
-};
-
-/**
- * Jest allows functions and classes to be used as test names in test() and
- * describe()
- */
-type JestTestName = string | Function;
-
-/**
- * Plugin: jest-styled-components
- */
-
-type JestStyledComponentsMatcherValue =
- | string
- | JestAsymmetricEqualityType
- | RegExp
- | typeof undefined;
-
-type JestStyledComponentsMatcherOptions = {
- media?: string;
- modifier?: string;
- supports?: string;
-}
-
-type JestStyledComponentsMatchersType = {
- toHaveStyleRule(
- property: string,
- value: JestStyledComponentsMatcherValue,
- options?: JestStyledComponentsMatcherOptions
- ): void,
-};
-
-/**
- * Plugin: jest-enzyme
- */
-type EnzymeMatchersType = {
- toBeChecked(): void,
- toBeDisabled(): void,
- toBeEmpty(): void,
- toBeEmptyRender(): void,
- toBePresent(): void,
- toContainReact(element: React$Element): void,
- toExist(): void,
- toHaveClassName(className: string): void,
- toHaveHTML(html: string): void,
- toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void),
- toHaveRef(refName: string): void,
- toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void),
- toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void),
- toHaveTagName(tagName: string): void,
- toHaveText(text: string): void,
- toIncludeText(text: string): void,
- toHaveValue(value: any): void,
- toMatchElement(element: React$Element): void,
- toMatchSelector(selector: string): void
-};
-
-// DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers
-type DomTestingLibraryType = {
- toBeInTheDOM(): void,
- toHaveTextContent(content: string): void,
- toHaveAttribute(name: string, expectedValue?: string): void
-};
-
-// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers
-type JestJQueryMatchersType = {
- toExist(): void,
- toHaveLength(len: number): void,
- toHaveId(id: string): void,
- toHaveClass(className: string): void,
- toHaveTag(tag: string): void,
- toHaveAttr(key: string, val?: any): void,
- toHaveProp(key: string, val?: any): void,
- toHaveText(text: string | RegExp): void,
- toHaveData(key: string, val?: any): void,
- toHaveValue(val: any): void,
- toHaveCss(css: {[key: string]: any}): void,
- toBeChecked(): void,
- toBeDisabled(): void,
- toBeEmpty(): void,
- toBeHidden(): void,
- toBeSelected(): void,
- toBeVisible(): void,
- toBeFocused(): void,
- toBeInDom(): void,
- toBeMatchedBy(sel: string): void,
- toHaveDescendant(sel: string): void,
- toHaveDescendantWithText(sel: string, text: string | RegExp): void
-};
-
-
-// Jest Extended Matchers: https://github.com/jest-community/jest-extended
-type JestExtendedMatchersType = {
- /**
- * Note: Currently unimplemented
- * Passing assertion
- *
- * @param {String} message
- */
- // pass(message: string): void;
-
- /**
- * Note: Currently unimplemented
- * Failing assertion
- *
- * @param {String} message
- */
- // fail(message: string): void;
-
- /**
- * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty.
- */
- toBeEmpty(): void;
-
- /**
- * Use .toBeOneOf when checking if a value is a member of a given Array.
- * @param {Array.<*>} members
- */
- toBeOneOf(members: any[]): void;
-
- /**
- * Use `.toBeNil` when checking a value is `null` or `undefined`.
- */
- toBeNil(): void;
-
- /**
- * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`.
- * @param {Function} predicate
- */
- toSatisfy(predicate: (n: any) => boolean): void;
-
- /**
- * Use `.toBeArray` when checking if a value is an `Array`.
- */
- toBeArray(): void;
-
- /**
- * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x.
- * @param {Number} x
- */
- toBeArrayOfSize(x: number): void;
-
- /**
- * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set.
- * @param {Array.<*>} members
- */
- toIncludeAllMembers(members: any[]): void;
-
- /**
- * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set.
- * @param {Array.<*>} members
- */
- toIncludeAnyMembers(members: any[]): void;
-
- /**
- * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array.
- * @param {Function} predicate
- */
- toSatisfyAll(predicate: (n: any) => boolean): void;
-
- /**
- * Use `.toBeBoolean` when checking if a value is a `Boolean`.
- */
- toBeBoolean(): void;
-
- /**
- * Use `.toBeTrue` when checking a value is equal (===) to `true`.
- */
- toBeTrue(): void;
-
- /**
- * Use `.toBeFalse` when checking a value is equal (===) to `false`.
- */
- toBeFalse(): void;
-
- /**
- * Use .toBeDate when checking if a value is a Date.
- */
- toBeDate(): void;
-
- /**
- * Use `.toBeFunction` when checking if a value is a `Function`.
- */
- toBeFunction(): void;
-
- /**
- * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`.
- *
- * Note: Required Jest version >22
- * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same
- *
- * @param {Mock} mock
- */
- toHaveBeenCalledBefore(mock: JestMockFn): void;
-
- /**
- * Use `.toBeNumber` when checking if a value is a `Number`.
- */
- toBeNumber(): void;
-
- /**
- * Use `.toBeNaN` when checking a value is `NaN`.
- */
- toBeNaN(): void;
-
- /**
- * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`.
- */
- toBeFinite(): void;
-
- /**
- * Use `.toBePositive` when checking if a value is a positive `Number`.
- */
- toBePositive(): void;
-
- /**
- * Use `.toBeNegative` when checking if a value is a negative `Number`.
- */
- toBeNegative(): void;
-
- /**
- * Use `.toBeEven` when checking if a value is an even `Number`.
- */
- toBeEven(): void;
-
- /**
- * Use `.toBeOdd` when checking if a value is an odd `Number`.
- */
- toBeOdd(): void;
-
- /**
- * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive).
- *
- * @param {Number} start
- * @param {Number} end
- */
- toBeWithin(start: number, end: number): void;
-
- /**
- * Use `.toBeObject` when checking if a value is an `Object`.
- */
- toBeObject(): void;
-
- /**
- * Use `.toContainKey` when checking if an object contains the provided key.
- *
- * @param {String} key
- */
- toContainKey(key: string): void;
-
- /**
- * Use `.toContainKeys` when checking if an object has all of the provided keys.
- *
- * @param {Array.} keys
- */
- toContainKeys(keys: string[]): void;
-
- /**
- * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys.
- *
- * @param {Array.} keys
- */
- toContainAllKeys(keys: string[]): void;
-
- /**
- * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys.
- *
- * @param {Array.} keys
- */
- toContainAnyKeys(keys: string[]): void;
-
- /**
- * Use `.toContainValue` when checking if an object contains the provided value.
- *
- * @param {*} value
- */
- toContainValue(value: any): void;
-
- /**
- * Use `.toContainValues` when checking if an object contains all of the provided values.
- *
- * @param {Array.<*>} values
- */
- toContainValues(values: any[]): void;
-
- /**
- * Use `.toContainAllValues` when checking if an object only contains all of the provided values.
- *
- * @param {Array.<*>} values
- */
- toContainAllValues(values: any[]): void;
-
- /**
- * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values.
- *
- * @param {Array.<*>} values
- */
- toContainAnyValues(values: any[]): void;
-
- /**
- * Use `.toContainEntry` when checking if an object contains the provided entry.
- *
- * @param {Array.} entry
- */
- toContainEntry(entry: [string, string]): void;
-
- /**
- * Use `.toContainEntries` when checking if an object contains all of the provided entries.
- *
- * @param {Array.>} entries
- */
- toContainEntries(entries: [string, string][]): void;
-
- /**
- * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries.
- *
- * @param {Array.>} entries
- */
- toContainAllEntries(entries: [string, string][]): void;
-
- /**
- * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries.
- *
- * @param {Array.>} entries
- */
- toContainAnyEntries(entries: [string, string][]): void;
-
- /**
- * Use `.toBeExtensible` when checking if an object is extensible.
- */
- toBeExtensible(): void;
-
- /**
- * Use `.toBeFrozen` when checking if an object is frozen.
- */
- toBeFrozen(): void;
-
- /**
- * Use `.toBeSealed` when checking if an object is sealed.
- */
- toBeSealed(): void;
-
- /**
- * Use `.toBeString` when checking if a value is a `String`.
- */
- toBeString(): void;
-
- /**
- * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings.
- *
- * @param {String} string
- */
- toEqualCaseInsensitive(string: string): void;
-
- /**
- * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix.
- *
- * @param {String} prefix
- */
- toStartWith(prefix: string): void;
-
- /**
- * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix.
- *
- * @param {String} suffix
- */
- toEndWith(suffix: string): void;
-
- /**
- * Use `.toInclude` when checking if a `String` includes the given `String` substring.
- *
- * @param {String} substring
- */
- toInclude(substring: string): void;
-
- /**
- * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times.
- *
- * @param {String} substring
- * @param {Number} times
- */
- toIncludeRepeated(substring: string, times: number): void;
-
- /**
- * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings.
- *
- * @param {Array.} substring
- */
- toIncludeMultiple(substring: string[]): void;
-};
-
-interface JestExpectType {
- not:
- & JestExpectType
- & EnzymeMatchersType
- & DomTestingLibraryType
- & JestJQueryMatchersType
- & JestStyledComponentsMatchersType
- & JestExtendedMatchersType,
- /**
- * If you have a mock function, you can use .lastCalledWith to test what
- * arguments it was last called with.
- */
- lastCalledWith(...args: Array): void,
- /**
- * toBe just checks that a value is what you expect. It uses === to check
- * strict equality.
- */
- toBe(value: any): void,
- /**
- * Use .toBeCalledWith to ensure that a mock function was called with
- * specific arguments.
- */
- toBeCalledWith(...args: Array): void,
- /**
- * Using exact equality with floating point numbers is a bad idea. Rounding
- * means that intuitive things fail.
- */
- toBeCloseTo(num: number, delta: any): void,
- /**
- * Use .toBeDefined to check that a variable is not undefined.
- */
- toBeDefined(): void,
- /**
- * Use .toBeFalsy when you don't care what a value is, you just want to
- * ensure a value is false in a boolean context.
- */
- toBeFalsy(): void,
- /**
- * To compare floating point numbers, you can use toBeGreaterThan.
- */
- toBeGreaterThan(number: number): void,
- /**
- * To compare floating point numbers, you can use toBeGreaterThanOrEqual.
- */
- toBeGreaterThanOrEqual(number: number): void,
- /**
- * To compare floating point numbers, you can use toBeLessThan.
- */
- toBeLessThan(number: number): void,
- /**
- * To compare floating point numbers, you can use toBeLessThanOrEqual.
- */
- toBeLessThanOrEqual(number: number): void,
- /**
- * Use .toBeInstanceOf(Class) to check that an object is an instance of a
- * class.
- */
- toBeInstanceOf(cls: Class<*>): void,
- /**
- * .toBeNull() is the same as .toBe(null) but the error messages are a bit
- * nicer.
- */
- toBeNull(): void,
- /**
- * Use .toBeTruthy when you don't care what a value is, you just want to
- * ensure a value is true in a boolean context.
- */
- toBeTruthy(): void,
- /**
- * Use .toBeUndefined to check that a variable is undefined.
- */
- toBeUndefined(): void,
- /**
- * Use .toContain when you want to check that an item is in a list. For
- * testing the items in the list, this uses ===, a strict equality check.
- */
- toContain(item: any): void,
- /**
- * Use .toContainEqual when you want to check that an item is in a list. For
- * testing the items in the list, this matcher recursively checks the
- * equality of all fields, rather than checking for object identity.
- */
- toContainEqual(item: any): void,
- /**
- * Use .toEqual when you want to check that two objects have the same value.
- * This matcher recursively checks the equality of all fields, rather than
- * checking for object identity.
- */
- toEqual(value: any): void,
- /**
- * Use .toHaveBeenCalled to ensure that a mock function got called.
- */
- toHaveBeenCalled(): void,
- toBeCalled(): void;
- /**
- * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact
- * number of times.
- */
- toHaveBeenCalledTimes(number: number): void,
- toBeCalledTimes(number: number): void;
- /**
- *
- */
- toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void;
- nthCalledWith(nthCall: number, ...args: Array): void;
- /**
- *
- */
- toHaveReturned(): void;
- toReturn(): void;
- /**
- *
- */
- toHaveReturnedTimes(number: number): void;
- toReturnTimes(number: number): void;
- /**
- *
- */
- toHaveReturnedWith(value: any): void;
- toReturnWith(value: any): void;
- /**
- *
- */
- toHaveLastReturnedWith(value: any): void;
- lastReturnedWith(value: any): void;
- /**
- *
- */
- toHaveNthReturnedWith(nthCall: number, value: any): void;
- nthReturnedWith(nthCall: number, value: any): void;
- /**
- * Use .toHaveBeenCalledWith to ensure that a mock function was called with
- * specific arguments.
- */
- toHaveBeenCalledWith(...args: Array): void,
- toBeCalledWith(...args: Array): void,
- /**
- * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called
- * with specific arguments.
- */
- toHaveBeenLastCalledWith(...args: Array): void,
- lastCalledWith(...args: Array): void,
- /**
- * Check that an object has a .length property and it is set to a certain
- * numeric value.
- */
- toHaveLength(number: number): void,
- /**
- *
- */
- toHaveProperty(propPath: string, value?: any): void,
- /**
- * Use .toMatch to check that a string matches a regular expression or string.
- */
- toMatch(regexpOrString: RegExp | string): void,
- /**
- * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object.
- */
- toMatchObject(object: Object | Array): void,
- /**
- * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object.
- */
- toStrictEqual(value: any): void,
- /**
- * This ensures that an Object matches the most recent snapshot.
- */
- toMatchSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, name?: string): void,
- /**
- * This ensures that an Object matches the most recent snapshot.
- */
- toMatchSnapshot(name: string): void,
-
- toMatchInlineSnapshot(snapshot?: string): void,
- toMatchInlineSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, snapshot?: string): void,
- /**
- * Use .toThrow to test that a function throws when it is called.
- * If you want to test that a specific error gets thrown, you can provide an
- * argument to toThrow. The argument can be a string for the error message,
- * a class for the error, or a regex that should match the error.
- *
- * Alias: .toThrowError
- */
- toThrow(message?: string | Error | Class | RegExp): void,
- toThrowError(message?: string | Error | Class | RegExp): void,
- /**
- * Use .toThrowErrorMatchingSnapshot to test that a function throws a error
- * matching the most recent snapshot when it is called.
- */
- toThrowErrorMatchingSnapshot(): void,
- toThrowErrorMatchingInlineSnapshot(snapshot?: string): void,
-}
-
-type JestObjectType = {
- /**
- * Disables automatic mocking in the module loader.
- *
- * After this method is called, all `require()`s will return the real
- * versions of each module (rather than a mocked version).
- */
- disableAutomock(): JestObjectType,
- /**
- * An un-hoisted version of disableAutomock
- */
- autoMockOff(): JestObjectType,
- /**
- * Enables automatic mocking in the module loader.
- */
- enableAutomock(): JestObjectType,
- /**
- * An un-hoisted version of enableAutomock
- */
- autoMockOn(): JestObjectType,
- /**
- * Clears the mock.calls and mock.instances properties of all mocks.
- * Equivalent to calling .mockClear() on every mocked function.
- */
- clearAllMocks(): JestObjectType,
- /**
- * Resets the state of all mocks. Equivalent to calling .mockReset() on every
- * mocked function.
- */
- resetAllMocks(): JestObjectType,
- /**
- * Restores all mocks back to their original value.
- */
- restoreAllMocks(): JestObjectType,
- /**
- * Removes any pending timers from the timer system.
- */
- clearAllTimers(): void,
- /**
- * The same as `mock` but not moved to the top of the expectation by
- * babel-jest.
- */
- doMock(moduleName: string, moduleFactory?: any): JestObjectType,
- /**
- * The same as `unmock` but not moved to the top of the expectation by
- * babel-jest.
- */
- dontMock(moduleName: string): JestObjectType,
- /**
- * Returns a new, unused mock function. Optionally takes a mock
- * implementation.
- */
- fn, TReturn>(
- implementation?: (...args: TArguments) => TReturn
- ): JestMockFn,
- /**
- * Determines if the given function is a mocked function.
- */
- isMockFunction(fn: Function): boolean,
- /**
- * Given the name of a module, use the automatic mocking system to generate a
- * mocked version of the module for you.
- */
- genMockFromModule(moduleName: string): any,
- /**
- * Mocks a module with an auto-mocked version when it is being required.
- *
- * The second argument can be used to specify an explicit module factory that
- * is being run instead of using Jest's automocking feature.
- *
- * The third argument can be used to create virtual mocks -- mocks of modules
- * that don't exist anywhere in the system.
- */
- mock(
- moduleName: string,
- moduleFactory?: any,
- options?: Object
- ): JestObjectType,
- /**
- * Returns the actual module instead of a mock, bypassing all checks on
- * whether the module should receive a mock implementation or not.
- */
- requireActual(moduleName: string): any,
- /**
- * Returns a mock module instead of the actual module, bypassing all checks
- * on whether the module should be required normally or not.
- */
- requireMock(moduleName: string): any,
- /**
- * Resets the module registry - the cache of all required modules. This is
- * useful to isolate modules where local state might conflict between tests.
- */
- resetModules(): JestObjectType,
- /**
- * Exhausts the micro-task queue (usually interfaced in node via
- * process.nextTick).
- */
- runAllTicks(): void,
- /**
- * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(),
- * setInterval(), and setImmediate()).
- */
- runAllTimers(): void,
- /**
- * Exhausts all tasks queued by setImmediate().
- */
- runAllImmediates(): void,
- /**
- * Executes only the macro task queue (i.e. all tasks queued by setTimeout()
- * or setInterval() and setImmediate()).
- */
- advanceTimersByTime(msToRun: number): void,
- /**
- * Executes only the macro task queue (i.e. all tasks queued by setTimeout()
- * or setInterval() and setImmediate()).
- *
- * Renamed to `advanceTimersByTime`.
- */
- runTimersToTime(msToRun: number): void,
- /**
- * Executes only the macro-tasks that are currently pending (i.e., only the
- * tasks that have been queued by setTimeout() or setInterval() up to this
- * point)
- */
- runOnlyPendingTimers(): void,
- /**
- * Explicitly supplies the mock object that the module system should return
- * for the specified module. Note: It is recommended to use jest.mock()
- * instead.
- */
- setMock(moduleName: string, moduleExports: any): JestObjectType,
- /**
- * Indicates that the module system should never return a mocked version of
- * the specified module from require() (e.g. that it should always return the
- * real module).
- */
- unmock(moduleName: string): JestObjectType,
- /**
- * Instructs Jest to use fake versions of the standard timer functions
- * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick,
- * setImmediate and clearImmediate).
- */
- useFakeTimers(): JestObjectType,
- /**
- * Instructs Jest to use the real versions of the standard timer functions.
- */
- useRealTimers(): JestObjectType,
- /**
- * Creates a mock function similar to jest.fn but also tracks calls to
- * object[methodName].
- */
- spyOn(object: Object, methodName: string, accessType?: "get" | "set"): JestMockFn,
- /**
- * Set the default timeout interval for tests and before/after hooks in milliseconds.
- * Note: The default timeout interval is 5 seconds if this method is not called.
- */
- setTimeout(timeout: number): JestObjectType
-};
-
-type JestSpyType = {
- calls: JestCallsType
-};
-
-/** Runs this function after every test inside this context */
-declare function afterEach(
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-/** Runs this function before every test inside this context */
-declare function beforeEach(
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-/** Runs this function after all tests have finished inside this context */
-declare function afterAll(
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-/** Runs this function before any tests have started inside this context */
-declare function beforeAll(
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-
-/** A context for grouping tests together */
-declare var describe: {
- /**
- * Creates a block that groups together several related tests in one "test suite"
- */
- (name: JestTestName, fn: () => void): void,
-
- /**
- * Only run this describe block
- */
- only(name: JestTestName, fn: () => void): void,
-
- /**
- * Skip running this describe block
- */
- skip(name: JestTestName, fn: () => void): void
-};
-
-/** An individual test unit */
-declare var it: {
- /**
- * An individual test unit
- *
- * @param {JestTestName} Name of Test
- * @param {Function} Test
- * @param {number} Timeout for the test, in milliseconds.
- */
- (
- name: JestTestName,
- fn?: (done: () => void) => ?Promise,
- timeout?: number
- ): void,
- /**
- * each runs this test against array of argument arrays per each run
- *
- * @param {table} table of Test
- */
- each(
- table: Array>
- ): (
- name: JestTestName,
- fn?: (...args: Array) => ?Promise
- ) => void,
- /**
- * Only run this test
- *
- * @param {JestTestName} Name of Test
- * @param {Function} Test
- * @param {number} Timeout for the test, in milliseconds.
- */
- only(
- name: JestTestName,
- fn?: (done: () => void) => ?Promise,
- timeout?: number
- ): {
- each(
- table: Array>
- ): (
- name: JestTestName,
- fn?: (...args: Array) => ?Promise
- ) => void,
- },
- /**
- * Skip running this test
- *
- * @param {JestTestName} Name of Test
- * @param {Function} Test
- * @param {number} Timeout for the test, in milliseconds.
- */
- skip(
- name: JestTestName,
- fn?: (done: () => void) => ?Promise,
- timeout?: number
- ): void,
- /**
- * Run the test concurrently
- *
- * @param {JestTestName} Name of Test
- * @param {Function} Test
- * @param {number} Timeout for the test, in milliseconds.
- */
- concurrent(
- name: JestTestName,
- fn?: (done: () => void) => ?Promise,
- timeout?: number
- ): void
-};
-declare function fit(
- name: JestTestName,
- fn: (done: () => void) => ?Promise,
- timeout?: number
-): void;
-/** An individual test unit */
-declare var test: typeof it;
-/** A disabled group of tests */
-declare var xdescribe: typeof describe;
-/** A focused group of tests */
-declare var fdescribe: typeof describe;
-/** A disabled individual test */
-declare var xit: typeof it;
-/** A disabled individual test */
-declare var xtest: typeof it;
-
-type JestPrettyFormatColors = {
- comment: { close: string, open: string },
- content: { close: string, open: string },
- prop: { close: string, open: string },
- tag: { close: string, open: string },
- value: { close: string, open: string },
-};
-
-type JestPrettyFormatIndent = string => string;
-type JestPrettyFormatRefs = Array;
-type JestPrettyFormatPrint = any => string;
-type JestPrettyFormatStringOrNull = string | null;
-
-type JestPrettyFormatOptions = {|
- callToJSON: boolean,
- edgeSpacing: string,
- escapeRegex: boolean,
- highlight: boolean,
- indent: number,
- maxDepth: number,
- min: boolean,
- plugins: JestPrettyFormatPlugins,
- printFunctionName: boolean,
- spacing: string,
- theme: {|
- comment: string,
- content: string,
- prop: string,
- tag: string,
- value: string,
- |},
-|};
-
-type JestPrettyFormatPlugin = {
- print: (
- val: any,
- serialize: JestPrettyFormatPrint,
- indent: JestPrettyFormatIndent,
- opts: JestPrettyFormatOptions,
- colors: JestPrettyFormatColors,
- ) => string,
- test: any => boolean,
-};
-
-type JestPrettyFormatPlugins = Array;
-
-/** The expect function is used every time you want to test a value */
-declare var expect: {
- /** The object that you want to make assertions against */
- (value: any):
- & JestExpectType
- & JestPromiseType
- & EnzymeMatchersType
- & DomTestingLibraryType
- & JestJQueryMatchersType
- & JestStyledComponentsMatchersType
- & JestExtendedMatchersType,
-
- /** Add additional Jasmine matchers to Jest's roster */
- extend(matchers: { [name: string]: JestMatcher }): void,
- /** Add a module that formats application-specific data structures. */
- addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void,
- assertions(expectedAssertions: number): void,
- hasAssertions(): void,
- any(value: mixed): JestAsymmetricEqualityType,
- anything(): any,
- arrayContaining(value: Array): Array,
- objectContaining(value: Object): Object,
- /** Matches any received string that contains the exact expected string. */
- stringContaining(value: string): string,
- stringMatching(value: string | RegExp): string,
- not: {
- arrayContaining: (value: $ReadOnlyArray) => Array,
- objectContaining: (value: {}) => Object,
- stringContaining: (value: string) => string,
- stringMatching: (value: string | RegExp) => string,
- },
-};
-
-// TODO handle return type
-// http://jasmine.github.io/2.4/introduction.html#section-Spies
-declare function spyOn(value: mixed, method: string): Object;
-
-/** Holds all functions related to manipulating test runner */
-declare var jest: JestObjectType;
-
-/**
- * The global Jasmine object, this is generally not exposed as the public API,
- * using features inside here could break in later versions of Jest.
- */
-declare var jasmine: {
- DEFAULT_TIMEOUT_INTERVAL: number,
- any(value: mixed): JestAsymmetricEqualityType,
- anything(): any,
- arrayContaining(value: Array): Array