diff --git a/Jenkinsfile b/Jenkinsfile index c759de5733..37cbdb4064 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -55,7 +55,13 @@ node('docker') { if (isMainBranch()) { stage('Lifecycle') { - nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' + try { + // failBuildOnNetworkError -> so we can catch the exception and neither fail nor make our build unstable + nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build', failBuildOnNetworkError: true + } catch (Exception e) { + echo "ERROR: iQ Server policy eval failed. Not marking build unstable for now." + echo "ERROR: iQ Server Exception: ${e.getMessage()}" + } } stage('Archive') { diff --git a/scm-core/src/main/java/sonia/scm/repository/FileObject.java b/scm-core/src/main/java/sonia/scm/repository/FileObject.java index 7dedebb13a..8f1cf298de 100644 --- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java +++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java @@ -46,8 +46,11 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; import static java.util.Collections.unmodifiableCollection; +import static java.util.Optional.ofNullable; /** * The FileObject represents a file or a directory in a repository. @@ -90,7 +93,9 @@ public class FileObject implements LastModifiedAware, Serializable && Objects.equal(description, other.description) && Objects.equal(length, other.length) && Objects.equal(subRepository, other.subRepository) - && Objects.equal(lastModified, other.lastModified); + && Objects.equal(commitDate, other.commitDate) + && Objects.equal(partialResult, other.partialResult) + && Objects.equal(computationAborted, other.computationAborted); //J+ } @@ -100,8 +105,16 @@ public class FileObject implements LastModifiedAware, Serializable @Override public int hashCode() { - return Objects.hashCode(name, path, directory, description, length, - subRepository, lastModified); + return Objects.hashCode( + name, + path, + directory, + description, + length, + subRepository, + commitDate, + partialResult, + computationAborted); } /** @@ -118,7 +131,9 @@ public class FileObject implements LastModifiedAware, Serializable .add("description", description) .add("length", length) .add("subRepository", subRepository) - .add("lastModified", lastModified) + .add("commitDate", commitDate) + .add("partialResult", partialResult) + .add("computationAborted", computationAborted) .toString(); //J+ } @@ -130,35 +145,44 @@ public class FileObject implements LastModifiedAware, Serializable * if the repository provider is not able to get the last commit for the path. * * - * @return last commit message + * @return Last commit message or null, when this value has not been computed + * (see {@link #isPartialResult()}). */ - public String getDescription() + public Optional getDescription() { - return description; + return ofNullable(description); } /** * Returns the last commit date for this. The method will return null, - * if the repository provider is not able to get the last commit for the path. + * if the repository provider is not able to get the last commit for the path + * or it has not been computed. * * * @return last commit date */ @Override - public Long getLastModified() - { - return lastModified; + public Long getLastModified() { + return this.isPartialResult()? null: this.commitDate; } /** - * Returns the length of the file. - * - * - * @return length of file + * Returns the last commit date for this. The method will return {@link OptionalLong#empty()}, + * if the repository provider is not able to get the last commit for the path or if this value has not been computed + * (see {@link #isPartialResult()} and {@link #isComputationAborted()}). */ - public long getLength() + public OptionalLong getCommitDate() { - return length; + return commitDate == null? OptionalLong.empty(): OptionalLong.of(commitDate); + } + + /** + * Returns the length of the file or {@link OptionalLong#empty()}, when this value has not been computed + * (see {@link #isPartialResult()} and {@link #isComputationAborted()}). + */ + public OptionalLong getLength() + { + return length == null? OptionalLong.empty(): OptionalLong.of(length); } /** @@ -200,7 +224,7 @@ public class FileObject implements LastModifiedAware, Serializable } /** - * Return sub repository informations or null if the file is not + * Return sub repository information or null if the file is not * sub repository. * * @since 1.10 @@ -222,6 +246,42 @@ public class FileObject implements LastModifiedAware, Serializable return directory; } + /** + * Returns the children of this file. + * + * @return The children of this file if it is a directory. + */ + public Collection getChildren() { + return children == null? null: unmodifiableCollection(children); + } + + /** + * If this is true, some values for this object have not been computed, yet. These values (like + * {@link #getLength()}, {@link #getDescription()} or {@link #getCommitDate()}) + * will return {@link Optional#empty()} (or {@link OptionalLong#empty()} respectively), unless they are computed. + * There may be an asynchronous task running, that will set these values in the future. + * + * @since 2.0.0 + * + * @return true, whenever some values of this object have not been computed, yet. + */ + public boolean isPartialResult() { + return partialResult; + } + + /** + * If this is true, some values for this object have not been computed and will not be computed. These + * values (like {@link #getLength()}, {@link #getDescription()} or {@link #getCommitDate()}) + * will return {@link Optional#empty()} (or {@link OptionalLong#empty()} respectively), unless they are computed. + * + * @since 2.0.0 + * + * @return true, whenever some values of this object finally are not computed. + */ + public boolean isComputationAborted() { + return computationAborted; + } + //~--- set methods ---------------------------------------------------------- /** @@ -247,14 +307,14 @@ public class FileObject implements LastModifiedAware, Serializable } /** - * Sets the last modified date of the file. + * Sets the commit date of the file. * * - * @param lastModified last modified date + * @param commitDate commit date */ - public void setLastModified(Long lastModified) + public void setCommitDate(Long commitDate) { - this.lastModified = lastModified; + this.commitDate = commitDate; } /** @@ -263,7 +323,7 @@ public class FileObject implements LastModifiedAware, Serializable * * @param length file length */ - public void setLength(long length) + public void setLength(Long length) { this.length = length; } @@ -302,22 +362,47 @@ public class FileObject implements LastModifiedAware, Serializable this.subRepository = subRepository; } - public Collection getChildren() { - return unmodifiableCollection(children); + /** + * Set marker, that some values for this object are not computed, yet. + * + * @since 2.0.0 + * + * @param partialResult Set this to true, whenever some values of this object are not computed, yet. + */ + public void setPartialResult(boolean partialResult) { + this.partialResult = partialResult; } + /** + * Set marker, that computation of some values for this object has been aborted. + * + * @since 2.0.0 + * + * @param computationAborted Set this to true, whenever some values of this object are not computed and + * will not be computed in the future. + */ + public void setComputationAborted(boolean computationAborted) { + this.computationAborted = computationAborted; + } + + /** + * Set the children for this file. + * + * @param children The new childre. + */ public void setChildren(List children) { this.children = new ArrayList<>(children); } + /** + * Adds a child to the list of children . + * + * @param child The additional child. + */ public void addChild(FileObject child) { this.children.add(child); } - public boolean hasChildren() { - return !children.isEmpty(); - } - //~--- fields --------------------------------------------------------------- /** file description */ @@ -326,11 +411,11 @@ public class FileObject implements LastModifiedAware, Serializable /** directory indicator */ private boolean directory; - /** last modified date */ - private Long lastModified; + /** commit date */ + private Long commitDate; /** file length */ - private long length; + private Long length; /** filename */ private String name; @@ -338,9 +423,16 @@ public class FileObject implements LastModifiedAware, Serializable /** file path */ private String path; + /** Marker for partial result. */ + private boolean partialResult = false; + + /** Marker for aborted computation. */ + private boolean computationAborted = false; + /** sub repository informations */ @XmlElement(name = "subrepository") private SubRepository subRepository; + /** Children of this file (aka directory). */ private Collection children = new ArrayList<>(); } 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 d482c04ea4..563557f0c1 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 @@ -199,7 +199,7 @@ public final class BrowseCommandBuilder return this; } - + /** * Disabling the last commit means that every call to * {@link FileObject#getDescription()} and @@ -300,6 +300,13 @@ public final class BrowseCommandBuilder return this; } + private void updateCache(BrowserResult updatedResult) { + if (!disableCache) { + CacheKey key = new CacheKey(repository, request); + cache.put(key, updatedResult); + } + } + //~--- inner classes -------------------------------------------------------- /** @@ -416,5 +423,5 @@ public final class BrowseCommandBuilder private final Repository repository; /** request for the command */ - private final BrowseCommandRequest request = new BrowseCommandRequest(); + private final BrowseCommandRequest request = new BrowseCommandRequest(this::updateCache); } 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 index 53f712cddc..1e6d5c6447 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java @@ -12,18 +12,25 @@ import static java.util.Collections.unmodifiableCollection; * case you can use {@link #getFilesWithConflict()} to get a list of files with merge conflicts. */ public class MergeCommandResult { + private final Collection filesWithConflict; + private final String newHeadRevision; + private final String targetRevision; + private final String revisionToMerge; - private MergeCommandResult(Collection filesWithConflict) { + private MergeCommandResult(Collection filesWithConflict, String targetRevision, String revisionToMerge, String newHeadRevision) { this.filesWithConflict = filesWithConflict; + this.targetRevision = targetRevision; + this.revisionToMerge = revisionToMerge; + this.newHeadRevision = newHeadRevision; } - public static MergeCommandResult success() { - return new MergeCommandResult(emptyList()); + public static MergeCommandResult success(String targetRevision, String revisionToMerge, String newHeadRevision) { + return new MergeCommandResult(emptyList(), targetRevision, revisionToMerge, newHeadRevision); } - public static MergeCommandResult failure(Collection filesWithConflict) { - return new MergeCommandResult(new HashSet<>(filesWithConflict)); + public static MergeCommandResult failure(String targetRevision, String revisionToMerge, Collection filesWithConflict) { + return new MergeCommandResult(new HashSet<>(filesWithConflict), targetRevision, revisionToMerge, null); } /** @@ -31,7 +38,7 @@ public class MergeCommandResult { * merge conflicts. In this case you can use {@link #getFilesWithConflict()} to check what files could not be merged. */ public boolean isSuccess() { - return filesWithConflict.isEmpty(); + return filesWithConflict.isEmpty() && newHeadRevision != null; } /** @@ -41,4 +48,26 @@ public class MergeCommandResult { public Collection getFilesWithConflict() { return unmodifiableCollection(filesWithConflict); } + + /** + * Returns the revision of the new head of the target branch, if the merge was successful ({@link #isSuccess()}) + */ + public String getNewHeadRevision() { + return newHeadRevision; + } + + /** + * Returns the revision of the target branch prior to the merge. + */ + public String getTargetRevision() { + return targetRevision; + } + + /** + * Returns the revision of the branch that was merged into the target (or in case of a conflict of the revision that + * should have been merged). + */ + public String getRevisionToMerge() { + return revisionToMerge; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 39da9a9ace..9c23fe93f2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -37,6 +37,10 @@ package sonia.scm.repository.spi; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import sonia.scm.repository.BrowserResult; + +import java.util.function.Consumer; + /** * * @author Sebastian Sdorra @@ -48,6 +52,14 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** Field description */ private static final long serialVersionUID = 7956624623516803183L; + public BrowseCommandRequest() { + this(null); + } + + public BrowseCommandRequest(Consumer updater) { + this.updater = updater; + } + //~--- methods -------------------------------------------------------------- /** @@ -220,6 +232,12 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest return recursive; } + public void updateCache(BrowserResult update) { + if (updater != null) { + updater.accept(update); + } + } + //~--- fields --------------------------------------------------------------- /** disable last commit */ @@ -230,4 +248,8 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** browse file objects recursive */ private boolean recursive = false; + + // WARNING / TODO: This field creates a reverse channel from the implementation to the API. This will break + // whenever the API runs in a different process than the SPI (for example to run explicit hosts for git repositories). + private final transient Consumer updater; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java index 12de0722a4..176d1772cf 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java @@ -7,6 +7,12 @@ import sonia.scm.repository.api.MergeStrategy; import java.util.Set; public interface MergeCommand { + /** + * Executes the merge. + * @param request The parameters specifying the merge. + * @return Result holding either the new revision or a list of conflicting files. + * @throws sonia.scm.NoChangesMadeException If the merge neither had a conflict nor made any change. + */ MergeCommandResult merge(MergeCommandRequest request); MergeDryRunCommandResult dryRun(MergeCommandRequest request); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java new file mode 100644 index 0000000000..a9e7d85dcc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java @@ -0,0 +1,83 @@ +package sonia.scm.repository.spi; + +import java.util.function.Consumer; + +/** + * Tasks submitted to this executor will be run synchronously up to a given time, after which they will be queued and + * processed asynchronously. After a maximum amount of time consumed by these tasks, they will be skipped. Note that + * this only works for short-living tasks. + *

+ * Get instances of this using a {@link SyncAsyncExecutorProvider}. + */ +public interface SyncAsyncExecutor { + + /** + * Execute the given task (either synchronously or asynchronously). If this task is skipped due to + * timeouts, nothing will be done. + * + * @param task The {@link Runnable} to be executed. + * @return Either {@link ExecutionType#SYNCHRONOUS} when the given {@link Runnable} has been executed immediately or + * {@link ExecutionType#ASYNCHRONOUS}, when the task was queued to be executed asynchronously in the future. + */ + default ExecutionType execute(Runnable task) { + return execute( + ignored -> task.run(), + () -> {} + ); + } + + /** + * Execute the given task (either synchronously or asynchronously). If this task is + * skipped due to timeouts, the abortionFallback will be called. + * + * @param task The {@link Runnable} to be executed. + * @param abortionFallback This will only be run, when this and all remaining tasks are aborted. This task should + * only consume a negligible amount of time. + * @return Either {@link ExecutionType#SYNCHRONOUS} when the given {@link Runnable} has been executed immediately or + * {@link ExecutionType#ASYNCHRONOUS}, when the task was queued to be executed asynchronously in the future. + */ + default ExecutionType execute(Runnable task, Runnable abortionFallback) { + return execute(ignored -> task.run(), abortionFallback); + } + + /** + * Execute the given task (either synchronously or asynchronously). If this task is skipped due to + * timeouts, nothing will be done. + * + * @param task The {@link Consumer} to be executed. The parameter given to this is either + * {@link ExecutionType#SYNCHRONOUS} when the given {@link Consumer} is executed immediately + * or {@link ExecutionType#ASYNCHRONOUS}, when the task had been queued and now is executed + * asynchronously. + * @return Either {@link ExecutionType#SYNCHRONOUS} when the given {@link Runnable} has been executed immediately or + * {@link ExecutionType#ASYNCHRONOUS}, when the task was queued to be executed asynchronously in the future. + */ + default ExecutionType execute(Consumer task) { + return execute(task, () -> {}); + } + + /** + * Execute the given task (either synchronously or asynchronously). If this task is + * skipped due to timeouts, the abortionFallback will be called. + * + * @param task The {@link Consumer} to be executed. The parameter given to this is either + * {@link ExecutionType#SYNCHRONOUS} when the given {@link Consumer} is executed immediately + * or {@link ExecutionType#ASYNCHRONOUS}, when the task had been queued and now is executed + * asynchronously. + * @param abortionFallback This will only be run, when this and all remaining tasks are aborted. This task should + * only consume a negligible amount of time. + * @return Either {@link ExecutionType#SYNCHRONOUS} when the given {@link Runnable} has been executed immediately or + * {@link ExecutionType#ASYNCHRONOUS}, when the task was queued to be executed asynchronously in the future. + */ + ExecutionType execute(Consumer task, Runnable abortionFallback); + + /** + * When all submitted tasks have been executed synchronously, this will return true. If at least one task + * has been enqueued to be executed asynchronously, this returns false (even when none of the enqueued + * tasks have been run, yet). + */ + boolean hasExecutedAllSynchronously(); + + enum ExecutionType { + SYNCHRONOUS, ASYNCHRONOUS + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java new file mode 100644 index 0000000000..5f417f324e --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java @@ -0,0 +1,56 @@ +package sonia.scm.repository.spi; + +/** + * Use this provider to get {@link SyncAsyncExecutor} instances to execute a number of normally short-lived tasks, that + * should be run asynchronously (or even be skipped) whenever they take too long in summary. + *

+ * The goal of this is a "best effort" approach: The submitted tasks are run immediately when they are submitted, unless + * a given timespan (switchToAsyncInSeconds) has passed. From this moment on the tasks are put into a queue to be + * processed asynchronously. If even then they take too long and their accumulated asynchronous runtime exceeds another + * limit (maxAsyncAbortSeconds), the tasks are skipped. + *

+ * Note that whenever a task has been started either synchronously or asynchronously it will neither be terminated nor + * switched from foreground to background execution, so this will only work well for short-living tasks. A long running + * task can still block this for longer than the configured amount of seconds. + */ +public interface SyncAsyncExecutorProvider { + + int DEFAULT_SWITCH_TO_ASYNC_IN_SECONDS = 2; + + /** + * Creates an {@link SyncAsyncExecutor} that will run tasks synchronously for + * {@link #DEFAULT_SWITCH_TO_ASYNC_IN_SECONDS} seconds. The limit of asynchronous runtime is implementation dependant. + * + * @return The executor. + */ + default SyncAsyncExecutor createExecutorWithDefaultTimeout() { + return createExecutorWithSecondsToTimeout(DEFAULT_SWITCH_TO_ASYNC_IN_SECONDS); + } + + /** + * Creates an {@link SyncAsyncExecutor} that will run tasks synchronously for + * switchToAsyncInSeconds seconds. The limit of asynchronous runtime is implementation dependant. + * + * @param switchToAsyncInSeconds The amount of seconds submitted tasks will be run synchronously. After this time, + * further tasks will be run asynchronously. To run all tasks asynchronously no matter + * what, set this to 0. + * @return The executor. + */ + SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds); + + /** + * Creates an {@link SyncAsyncExecutor} that will run tasks synchronously for + * switchToAsyncInSeconds seconds and will abort tasks after they ran + * maxAsyncAbortSeconds asynchronously. + * + * @param switchToAsyncInSeconds The amount of seconds submitted tasks will be run synchronously. After this time, + * further tasks will be run asynchronously. To run all tasks asynchronously no matter + * what, set this to 0. + * @param maxAsyncAbortSeconds The amount of seconds, tasks that were started asynchronously may run in summary + * before remaining tasks will not be executed at all anymore. To abort all tasks that + * are submitted after switchToAsyncInSeconds immediately, set this to + * 0. + * @return The executor. + */ + SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds, int maxAsyncAbortSeconds); +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java index 135221ea82..e4334d42b1 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java @@ -8,6 +8,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; +/** + * CopyOnWrite creates a copy of the target file, before it is modified. This should prevent empty or incomplete files + * on errors such as full disk. + * + * javasecurity:S2083: SonarQube thinks that the path (targetFile) is generated from an http header (HttpUtil), but + * this is not true. It looks like a false-positive, so we suppress the warning for now. + */ +@SuppressWarnings("javasecurity:S2083") public final class CopyOnWrite { private static final Logger LOG = LoggerFactory.getLogger(CopyOnWrite.class); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java index 9a30cc9f3e..61b5dbede3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java @@ -81,19 +81,19 @@ public class GitGcTask implements Runnable { { if (repository.isValid() && repository.isHealthy()) { - logger.info("start git gc for repository {}", repository.getName()); + logger.info("start git gc for repository {}", repository.getNamespaceAndName()); Stopwatch sw = Stopwatch.createStarted(); gc(repository); - logger.debug("gc of repository {} has finished after {}", repository.getName(), sw.stop()); + logger.debug("gc of repository {} has finished after {}", repository.getNamespaceAndName(), sw.stop()); } else { - logger.debug("skip non valid/healthy repository {}", repository.getName()); + logger.debug("skip non valid/healthy repository {}", repository.getNamespaceAndName()); } } else { - logger.trace("skip non git repository {}", repository.getName()); + logger.trace("skip non git repository {}", repository.getNamespaceAndName()); } } 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 d726b992ca..a93c1b5d81 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 @@ -745,6 +745,10 @@ public final class GitUtil public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) throws IOException { Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit); + return getLfsPointer(repo, treeWalk, attributes); + } + + public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, Attributes attributes) throws IOException { Attribute filter = attributes.get("filter"); if (filter != null && "lfs".equals(filter.getValue())) { ObjectId blobId = treeWalk.getObjectId(0); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index adf7878221..1c807c6fc7 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -186,6 +186,10 @@ class AbstractGitCommand return context; } + sonia.scm.repository.Repository getRepository() { + return repository; + } + void checkOutBranch(String branchName) throws IOException { try { clone.checkout().setName(branchName).call(); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java index 0204ca4e3c..fdecac6314 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -2,6 +2,7 @@ package sonia.scm.repository.spi; import com.google.common.base.Strings; import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; @@ -10,6 +11,8 @@ 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 sonia.scm.ContextEntry; +import sonia.scm.NotFoundException; import sonia.scm.repository.GitUtil; import sonia.scm.util.Util; @@ -35,49 +38,48 @@ final class Differ implements AutoCloseable { } private static Differ create(Repository repository, DiffCommandRequest request) throws IOException { - RevWalk walk = new RevWalk(repository); + RevWalk walk = new RevWalk(repository); - ObjectId revision = repository.resolve(request.getRevision()); - RevCommit commit = walk.parseCommit(revision); + ObjectId revision = repository.resolve(request.getRevision()); + if (revision == null) { + throw NotFoundException.notFound(ContextEntry.ContextBuilder.entity("revision not found", request.getRevision())); + } + RevCommit commit; + try { + commit = walk.parseCommit(revision); + } catch (MissingObjectException ex) { + throw NotFoundException.notFound(ContextEntry.ContextBuilder.entity("revision not found", request.getRevision())); + } - walk.markStart(commit); - commit = walk.next(); - TreeWalk treeWalk = new TreeWalk(repository); - treeWalk.reset(); - treeWalk.setRecursive(true); + walk.markStart(commit); + commit = walk.next(); + TreeWalk treeWalk = new TreeWalk(repository); + treeWalk.reset(); + treeWalk.setRecursive(true); - if (Util.isNotEmpty(request.getPath())) - { - treeWalk.setFilter(PathFilter.create(request.getPath())); - } + if (Util.isNotEmpty(request.getPath())) { + treeWalk.setFilter(PathFilter.create(request.getPath())); + } - if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) - { - ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); - ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision); - RevTree tree = walk.parseCommit(ancestorId).getTree(); + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) { + ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); + ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision); + RevTree tree = walk.parseCommit(ancestorId).getTree(); + treeWalk.addTree(tree); + } else if (commit.getParentCount() > 0) { + RevTree tree = commit.getParent(0).getTree(); + + if (tree != null) { treeWalk.addTree(tree); - } - else if (commit.getParentCount() > 0) - { - RevTree tree = commit.getParent(0).getTree(); - - if (tree != null) - { - treeWalk.addTree(tree); - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } - } - else - { + } else { treeWalk.addTree(new EmptyTreeIterator()); } + } else { + treeWalk.addTree(new EmptyTreeIterator()); + } - treeWalk.addTree(commit.getTree()); + treeWalk.addTree(commit.getTree()); return new Differ(commit, walk, treeWalk); } 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 aa362b8ec6..e8ef5a7a33 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 @@ -35,9 +35,11 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.lfs.LfsPointer; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; @@ -49,6 +51,7 @@ import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.eclipse.jgit.util.LfsFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.NotFoundException; @@ -56,6 +59,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.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.SubRepository; import sonia.scm.store.Blob; @@ -69,10 +73,13 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import static java.util.Optional.empty; +import static java.util.Optional.of; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS; //~--- JDK imports ------------------------------------------------------------ @@ -90,71 +97,56 @@ public class GitBrowseCommand extends AbstractGitCommand /** * the logger for GitBrowseCommand */ - private static final Logger logger = - LoggerFactory.getLogger(GitBrowseCommand.class); + private static final Logger logger = LoggerFactory.getLogger(GitBrowseCommand.class); + + /** sub repository cache */ + private final Map> subrepositoryCache = Maps.newHashMap(); + + private final Object asyncMonitor = new Object(); + private final LfsBlobStoreFactory lfsBlobStoreFactory; - //~--- constructors --------------------------------------------------------- + private final SyncAsyncExecutor executor; - /** - * Constructs ... - * @param context - * @param repository - * @param lfsBlobStoreFactory - */ - public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) - { + private BrowserResult browserResult; + + public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) { super(context, repository); this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.executor = executor; } - //~--- get methods ---------------------------------------------------------- - @Override - @SuppressWarnings("unchecked") public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { logger.debug("try to create browse result for {}", request); - BrowserResult result; - org.eclipse.jgit.lib.Repository repo = open(); - ObjectId revId; + ObjectId revId = computeRevIdToBrowse(request, repo); - if (Util.isEmpty(request.getRevision())) - { - revId = getDefaultBranch(repo); - } - else - { - revId = GitUtil.getRevisionId(repo, request.getRevision()); + if (revId != null) { + browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); + return browserResult; + } else { + logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); + return new BrowserResult(Constants.HEAD, request.getRevision(), createEmptyRoot()); } + } - if (revId != null) - { - result = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); - } - else - { - if (Util.isNotEmpty(request.getRevision())) - { + private ObjectId computeRevIdToBrowse(BrowseCommandRequest request, org.eclipse.jgit.lib.Repository repo) throws IOException { + if (Util.isEmpty(request.getRevision())) { + return getDefaultBranch(repo); + } else { + ObjectId revId = GitUtil.getRevisionId(repo, request.getRevision()); + if (revId == null) { logger.error("could not find revision {}", request.getRevision()); throw notFound(entity("Revision", request.getRevision()).in(this.repository)); } - else if (logger.isWarnEnabled()) - { - logger.warn("could not find head of repository, empty?"); - } - - result = new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); + return revId; } - - return result; } - //~--- methods -------------------------------------------------------------- - - private FileObject createEmtpyRoot() { + private FileObject createEmptyRoot() { FileObject fileObject = new FileObject(); fileObject.setName(""); fileObject.setPath(""); @@ -162,18 +154,6 @@ public class GitBrowseCommand extends AbstractGitCommand return fileObject; } - /** - * Method description - * - * @param repo - * @param request - * @param revId - * @param treeWalk - * - * @return - * - * @throws IOException - */ private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { @@ -207,127 +187,62 @@ public class GitBrowseCommand extends AbstractGitCommand // don't show message and date for directories to improve performance if (!file.isDirectory() &&!request.isDisableLastCommit()) { - logger.trace("fetch last commit for {} at {}", path, revId.getName()); - RevCommit commit = getLatestCommit(repo, revId, path); - - Optional lfsPointer = commit == null? empty(): GitUtil.getLfsPointer(repo, path, commit, treeWalk); + file.setPartialResult(true); + RevCommit commit; + try (RevWalk walk = new RevWalk(repo)) { + commit = walk.parseCommit(revId); + } + Optional lfsPointer = getLfsPointer(repo, path, commit, treeWalk); if (lfsPointer.isPresent()) { - BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); - String oid = lfsPointer.get().getOid().getName(); - Blob blob = lfsBlobStore.get(oid); - if (blob == null) { - logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName()); - file.setLength(-1); - } else { - file.setLength(blob.getSize()); - } + setFileLengthFromLfsBlob(lfsPointer.get(), file); } else { file.setLength(loader.getSize()); } - if (commit != null) - { - file.setLastModified(GitUtil.getCommitTime(commit)); - file.setDescription(commit.getShortMessage()); - } - else if (logger.isWarnEnabled()) - { - logger.warn("could not find latest commit for {} on {}", path, - revId); - } + executor.execute( + new CompleteFileInformation(path, revId, repo, file, request), + new AbortFileInformation(request) + ); } } return file; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * - * @param repo - * @param revId - * @param path - * - * @return - */ - private RevCommit getLatestCommit(org.eclipse.jgit.lib.Repository repo, - ObjectId revId, String path) - { - RevCommit result = null; - RevWalk walk = null; - - try - { - walk = new RevWalk(repo); - walk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path), - TreeFilter.ANY_DIFF)); - - RevCommit commit = walk.parseCommit(revId); - - walk.markStart(commit); - result = Util.getFirst(walk); - } - catch (IOException ex) - { - logger.error("could not parse commit for file", ex); - } - finally - { - GitUtil.release(walk); - } - - return result; + private void updateCache(BrowseCommandRequest request) { + request.updateCache(browserResult); + logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); } private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException { - RevWalk revWalk = null; - TreeWalk treeWalk = null; - - FileObject result; - - try { + try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo)) { logger.debug("load repository browser for revision {}", revId.name()); - treeWalk = new TreeWalk(repo); if (!isRootRequest(request)) { treeWalk.setFilter(PathFilter.create(request.getPath())); } - revWalk = new RevWalk(repo); RevTree tree = revWalk.parseTree(revId); - if (tree != null) - { + if (tree != null) { treeWalk.addTree(tree); - } - else - { + } else { throw new IllegalStateException("could not find tree for " + revId.name()); } if (isRootRequest(request)) { - result = createEmtpyRoot(); + FileObject result = createEmptyRoot(); findChildren(result, repo, request, revId, treeWalk); + return result; } else { - result = findFirstMatch(repo, request, revId, treeWalk); + FileObject result = findFirstMatch(repo, request, revId, treeWalk); if ( result.isDirectory() ) { treeWalk.enterSubtree(); findChildren(result, repo, request, revId, treeWalk); } + return result; } - } - finally - { - GitUtil.release(revWalk); - GitUtil.release(treeWalk); - } - - return result; } private boolean isRootRequest(BrowseCommandRequest request) { @@ -384,56 +299,144 @@ public class GitBrowseCommand extends AbstractGitCommand throw notFound(entity("File", request.getPath()).in("Revision", revId.getName()).in(this.repository)); } - @SuppressWarnings("unchecked") - private Map getSubRepositories(org.eclipse.jgit.lib.Repository repo, - ObjectId revision) + private Map getSubRepositories(org.eclipse.jgit.lib.Repository repo, ObjectId revision) throws IOException { - if (logger.isDebugEnabled()) - { - logger.debug("read submodules of {} at {}", repository.getName(), - revision); - } - Map subRepositories; - try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) - { + logger.debug("read submodules of {} at {}", repository.getName(), revision); + + try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) { new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision, PATH_MODULES, baos); - subRepositories = GitSubModuleParser.parse(baos.toString()); + return GitSubModuleParser.parse(baos.toString()); + } catch (NotFoundException ex) { + logger.trace("could not find .gitmodules: {}", ex.getMessage()); + return Collections.emptyMap(); } - catch (NotFoundException ex) - { - logger.trace("could not find .gitmodules", ex); - subRepositories = Collections.EMPTY_MAP; - } - - return subRepositories; } - private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo, - ObjectId revId, String path) + private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path) throws IOException { Map subRepositories = subrepositoryCache.get(revId); - if (subRepositories == null) - { + if (subRepositories == null) { subRepositories = getSubRepositories(repo, revId); subrepositoryCache.put(revId, subRepositories); } - SubRepository sub = null; - - if (subRepositories != null) - { - sub = subRepositories.get(path); + if (subRepositories != null) { + return subRepositories.get(path); } - - return sub; + return null; } - //~--- fields --------------------------------------------------------------- + private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) { + try { + Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit); - /** sub repository cache */ - private final Map> subrepositoryCache = Maps.newHashMap(); + return GitUtil.getLfsPointer(repo, treeWalk, attributes); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "could not read lfs pointer", e); + } + } + + private void setFileLengthFromLfsBlob(LfsPointer lfsPointer, FileObject file) { + BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); + String oid = lfsPointer.getOid().getName(); + Blob blob = lfsBlobStore.get(oid); + if (blob == null) { + logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName()); + file.setLength(null); + } else { + file.setLength(blob.getSize()); + } + } + + private class CompleteFileInformation implements Consumer { + private final String path; + private final ObjectId revId; + private final org.eclipse.jgit.lib.Repository repo; + private final FileObject file; + private final BrowseCommandRequest request; + + public CompleteFileInformation(String path, ObjectId revId, org.eclipse.jgit.lib.Repository repo, FileObject file, BrowseCommandRequest request) { + this.path = path; + this.revId = revId; + this.repo = repo; + this.file = file; + this.request = request; + } + + @Override + public void accept(SyncAsyncExecutor.ExecutionType executionType) { + logger.trace("fetch last commit for {} at {}", path, revId.getName()); + + Stopwatch sw = Stopwatch.createStarted(); + + Optional commit = getLatestCommit(repo, revId, path); + + synchronized (asyncMonitor) { + file.setPartialResult(false); + if (commit.isPresent()) { + applyValuesFromCommit(executionType, commit.get()); + } else { + logger.warn("could not find latest commit for {} on {}", path, revId); + } + } + + logger.trace("finished loading of last commit {} of {} in {}", revId.getName(), path, sw.stop()); + } + + private Optional getLatestCommit(org.eclipse.jgit.lib.Repository repo, + ObjectId revId, String path) { + try (RevWalk walk = new RevWalk(repo)) { + walk.setTreeFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, PathFilter.create(path))); + + RevCommit commit = walk.parseCommit(revId); + + walk.markStart(commit); + return of(Util.getFirst(walk)); + } catch (IOException ex) { + logger.error("could not parse commit for file", ex); + return empty(); + } + } + + private void applyValuesFromCommit(SyncAsyncExecutor.ExecutionType executionType, RevCommit commit) { + file.setCommitDate(GitUtil.getCommitTime(commit)); + file.setDescription(commit.getShortMessage()); + if (executionType == ASYNCHRONOUS && browserResult != null) { + updateCache(request); + } + } + } + + private class AbortFileInformation implements Runnable { + private final BrowseCommandRequest request; + + public AbortFileInformation(BrowseCommandRequest request) { + this.request = request; + } + + @Override + public void run() { + synchronized (asyncMonitor) { + if (markPartialAsAborted(browserResult.getFile())) { + updateCache(request); + } + } + } + + private boolean markPartialAsAborted(FileObject file) { + boolean changed = false; + if (file.isPartialResult()) { + file.setPartialResult(false); + file.setComputationAborted(true); + changed = true; + } + for (FileObject child : file.getChildren()) { + changed |= markPartialAsAborted(child); + } + return changed; + } + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java index 64a20a33cb..84ea1a0bbc 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java @@ -7,6 +7,7 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandResult; import java.io.IOException; +import java.util.Collections; class GitFastForwardIfPossible extends GitMergeStrategy { @@ -22,7 +23,7 @@ class GitFastForwardIfPossible extends GitMergeStrategy { MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); if (fastForwardResult.getMergeStatus().isSuccessful()) { push(); - return MergeCommandResult.success(); + return createSuccessResult(fastForwardResult.getNewHead().name()); } else { return fallbackMerge.run(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java index 6aa68a0ea8..9a7d290d3a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -3,10 +3,16 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.revwalk.RevCommit; +import sonia.scm.NoChangesMadeException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandResult; import java.io.IOException; +import java.util.Collections; +import java.util.Optional; + +import static sonia.scm.repository.spi.GitRevisionExtractor.extractRevisionFromRevCommit; class GitMergeCommit extends GitMergeStrategy { @@ -21,11 +27,12 @@ class GitMergeCommit extends GitMergeStrategy { MergeResult result = doMergeInClone(mergeCommand); if (result.getMergeStatus().isSuccessful()) { - doCommit(); + RevCommit revCommit = doCommit().orElseThrow(() -> new NoChangesMadeException(getRepository())); push(); - return MergeCommandResult.success(); + return createSuccessResult(extractRevisionFromRevCommit(revCommit)); } else { return analyseFailure(result); } } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java index 1d53b99c99..8e9f79d1b9 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -6,6 +6,7 @@ import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.InternalRepositoryException; @@ -14,6 +15,7 @@ import sonia.scm.repository.api.MergeCommandResult; import java.io.IOException; import java.text.MessageFormat; +import java.util.Optional; abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker { @@ -24,37 +26,57 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker doCommit() { + logger.debug("merged branch {} into {}", branchToMerge, targetBranch); + return doCommit(MessageFormat.format(determineMessageTemplate(), branchToMerge, targetBranch), author); + } + + MergeCommandResult createSuccessResult(String newRevision) { + return MergeCommandResult.success(targetRevision.name(), revisionToMerge.name(), newRevision); + } + + ObjectId getTargetRevision() { + return targetRevision; + } + + ObjectId getRevisionToMerge() { + return revisionToMerge; } private String determineMessageTemplate() { @@ -66,7 +88,7 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker new NoChangesMadeException(getRepository())); push(); - return MergeCommandResult.success(); + return MergeCommandResult.success(getTargetRevision().name(), revCommit.name(), extractRevisionFromRevCommit(revCommit)); } else { return analyseFailure(result); } 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 e1ae58ada5..fa54ca6007 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 @@ -80,12 +80,13 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { + public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) { this.handler = handler; this.repository = repository; this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.hookContextFactory = hookContextFactory; this.eventBus = eventBus; + this.executorProvider = executorProvider; this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); } @@ -150,7 +151,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public BrowseCommand getBrowseCommand() { - return new GitBrowseCommand(context, repository, lfsBlobStoreFactory); + return new GitBrowseCommand(context, repository, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout()); } /** @@ -301,4 +302,6 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider private final HookContextFactory hookContextFactory; private final ScmEventBus eventBus; + + private final SyncAsyncExecutorProvider executorProvider; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 7fc5fb27c4..1bb7e84b92 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -55,14 +55,16 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { private final LfsBlobStoreFactory lfsBlobStoreFactory; private final HookContextFactory hookContextFactory; private final ScmEventBus eventBus; + private final SyncAsyncExecutorProvider executorProvider; @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { + public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) { this.handler = handler; this.storeProvider = storeProvider; this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.hookContextFactory = hookContextFactory; this.eventBus = eventBus; + this.executorProvider = executorProvider; } @Override @@ -70,7 +72,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { GitRepositoryServiceProvider provider = null; if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus); + provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus, executorProvider); } return provider; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java new file mode 100644 index 0000000000..5055024151 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.revwalk.RevCommit; + +import java.util.Optional; + +public class GitRevisionExtractor { + + static String extractRevisionFromRevCommit(RevCommit revCommit) { + return revCommit.toString().split(" ")[1]; + } +} 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 4b854f6209..39066f0a9d 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 @@ -35,15 +35,21 @@ import org.junit.Test; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.repository.spi.SyncAsyncExecutors.AsyncExecutorStepper; import java.io.IOException; import java.util.Collection; +import java.util.LinkedList; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static sonia.scm.repository.spi.SyncAsyncExecutors.stepperAsynchronousExecutor; +import static sonia.scm.repository.spi.SyncAsyncExecutors.synchronousExecutor; /** * Unit tests for {@link GitBrowseCommand}. @@ -102,15 +108,55 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); assertEquals("a.txt", a.getPath()); - assertEquals("added new line for blame", a.getDescription()); - assertTrue(a.getLength() > 0); - checkDate(a.getLastModified()); + assertEquals("added new line for blame", a.getDescription().get()); + assertTrue(a.getLength().getAsLong() > 0); + checkDate(a.getCommitDate().getAsLong()); assertTrue(c.isDirectory()); assertEquals("c", c.getName()); assertEquals("c", c.getPath()); } + @Test + public void testAsynchronousBrowse() throws IOException { + try (AsyncExecutorStepper executor = stepperAsynchronousExecutor()) { + GitBrowseCommand command = new GitBrowseCommand(createContext(), repository, null, executor); + List updatedResults = new LinkedList<>(); + BrowseCommandRequest request = new BrowseCommandRequest(updatedResults::add); + FileObject root = command.getBrowserResult(request).getFile(); + assertNotNull(root); + + Collection foList = root.getChildren(); + + FileObject a = findFile(foList, "a.txt"); + FileObject b = findFile(foList, "b.txt"); + + assertTrue(a.isPartialResult()); + assertFalse("expected empty name before commit could have been read", a.getDescription().isPresent()); + assertFalse("expected empty date before commit could have been read", a.getCommitDate().isPresent()); + assertTrue(b.isPartialResult()); + assertFalse("expected empty name before commit could have been read", b.getDescription().isPresent()); + assertFalse("expected empty date before commit could have been read", b.getCommitDate().isPresent()); + + executor.next(); + + assertEquals(1, updatedResults.size()); + assertFalse(a.isPartialResult()); + assertNotNull("expected correct name after commit could have been read", a.getDescription()); + assertTrue("expected correct date after commit could have been read", a.getCommitDate().isPresent()); + assertTrue(b.isPartialResult()); + assertFalse("expected empty name before commit could have been read", b.getDescription().isPresent()); + assertFalse("expected empty date before commit could have been read", b.getCommitDate().isPresent()); + + executor.next(); + + assertEquals(2, updatedResults.size()); + assertFalse(b.isPartialResult()); + assertNotNull("expected correct name after commit could have been read", b.getDescription()); + assertTrue("expected correct date after commit could have been read", b.getCommitDate().isPresent()); + } + } + @Test public void testBrowseSubDirectory() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); @@ -129,20 +175,20 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertFalse(d.isDirectory()); assertEquals("d.txt", d.getName()); assertEquals("c/d.txt", d.getPath()); - assertEquals("added file d and e in folder c", d.getDescription()); - assertTrue(d.getLength() > 0); - checkDate(d.getLastModified()); + assertEquals("added file d and e in folder c", d.getDescription().get()); + assertTrue(d.getLength().getAsLong() > 0); + checkDate(d.getCommitDate().getAsLong()); assertFalse(e.isDirectory()); assertEquals("e.txt", e.getName()); assertEquals("c/e.txt", e.getPath()); - assertEquals("added file d and e in folder c", e.getDescription()); - assertTrue(e.getLength() > 0); - checkDate(e.getLastModified()); + assertEquals("added file d and e in folder c", e.getDescription().get()); + assertTrue(e.getLength().getAsLong() > 0); + checkDate(e.getCommitDate().getAsLong()); } @Test - public void testRecusive() throws IOException { + public void testRecursive() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setRecursive(true); @@ -171,6 +217,6 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } private GitBrowseCommand createCommand() { - return new GitBrowseCommand(createContext(), repository, null); + return new GitBrowseCommand(createContext(), repository, null, synchronousExecutor()); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index 52932e83ae..26d6fc7c13 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -37,6 +37,20 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { "+++ b/f.txt\n" + "@@ -0,0 +1 @@\n" + "+f\n"; + public static final String DIFF_FILE_PARTIAL_MERGE = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..8cd63ec 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1,2 @@\n" + + " a\n" + + "+change\n" + + "diff --git a/b.txt b/b.txt\n" + + "index 6178079..09ccdf0 100644\n" + + "--- a/b.txt\n" + + "+++ b/b.txt\n" + + "@@ -1 +1,2 @@\n" + + " b\n" + + "+change\n"; @Test public void diffForOneRevisionShouldCreateDiff() throws IOException { @@ -91,4 +105,15 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString()); } + + @Test + public void diffBetweenTwoBranchesWithMergedIntegrationBranchShouldCreateDiffOfAllIncomingChanges() throws IOException { + GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("partially_merged"); + diffCommandRequest.setAncestorChangeset("master"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); + assertEquals(DIFF_FILE_PARTIAL_MERGE, output.toString()); + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index fcd721c3a2..2616d5b6e5 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -12,6 +12,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Rule; import org.junit.Test; +import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.repository.Person; import sonia.scm.repository.api.MergeCommandResult; @@ -70,6 +71,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { MergeCommandResult mergeCommandResult = command.merge(request); assertThat(mergeCommandResult.isSuccess()).isTrue(); + assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo("91b99de908fcd04772798a31c308a64aea1a5523"); + assertThat(mergeCommandResult.getTargetRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); Repository repository = createContext().open(); Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); @@ -106,7 +109,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { assertThat(mergeCommit.getParent(1).name()).isEqualTo("d81ad6c63d7e2162308d69637b339dedd1d9201c"); } - @Test + @Test(expected = NoChangesMadeException.class) public void shouldNotMergeTwice() throws IOException, GitAPIException { GitMergeCommand command = createCommand(); MergeCommandRequest request = new MergeCommandRequest(); @@ -120,15 +123,9 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { assertThat(mergeCommandResult.isSuccess()).isTrue(); Repository repository = createContext().open(); - ObjectId firstMergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId(); + new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId(); - MergeCommandResult secondMergeCommandResult = command.merge(request); - - assertThat(secondMergeCommandResult.isSuccess()).isTrue(); - - ObjectId secondMergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId(); - - assertThat(secondMergeCommit).isEqualTo(firstMergeCommit); + command.merge(request); } @Test @@ -234,6 +231,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { Repository repository = createContext().open(); assertThat(mergeCommandResult.isSuccess()).isTrue(); + assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo(mergeCommandResult.getNewHeadRevision()); + assertThat(mergeCommandResult.getTargetRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); RevCommit mergeCommit = commits.iterator().next(); @@ -284,6 +283,9 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); MergeCommandResult mergeCommandResult = command.merge(request); + assertThat(mergeCommandResult.getNewHeadRevision()).isEqualTo("35597e9e98fe53167266583848bfef985c2adb27"); + assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo("35597e9e98fe53167266583848bfef985c2adb27"); + assertThat(mergeCommandResult.getTargetRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); assertThat(mergeCommandResult.isSuccess()).isTrue(); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommand_Conflict_Test.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommand_Conflict_Test.java index faaa553202..66c3e9f85e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommand_Conflict_Test.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommand_Conflict_Test.java @@ -17,16 +17,18 @@ public class GitMergeCommand_Conflict_Test extends AbstractGitCommandTestBase { static final String DIFF_HEADER = "diff --git a/Main.java b/Main.java"; static final String DIFF_FILE_CONFLICT = "--- a/Main.java\n" + "+++ b/Main.java\n" + - "@@ -3,7 +3,11 @@\n" + + "@@ -1,6 +1,13 @@\n" + + "+import java.util.Arrays;\n" + + "+\n" + " class Main {\n" + " public static void main(String[] args) {\n" + " System.out.println(\"Expect nothing more to happen.\");\n" + "+<<<<<<< HEAD\n" + - " System.out.println(\"Parameters:\");\n" + - " Arrays.stream(args).map(arg -> \"- \" + arg).forEach(System.out::println);\n" + + " System.out.println(\"This is for demonstration, only.\");\n" + "+=======\n" + - "+ System.out.println(\"This is for demonstration, only.\");\n" + - "+>>>>>>> integration\n" + + "+ System.out.println(\"Parameters:\");\n" + + "+ Arrays.stream(args).map(arg -> \"- \" + arg).forEach(System.out::println);\n" + + "+>>>>>>> feature/print_args\n" + " }\n" + " }"; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java new file mode 100644 index 0000000000..52a609b6cd --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java @@ -0,0 +1,21 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GitRevisionExtractorTest { + + @Test + void shouldReturnRevisionFromRevCommit() { + RevCommit revCommit = mock(RevCommit.class); + when(revCommit.toString()).thenReturn("commit 123456abcdef -t 4561"); + String revision = GitRevisionExtractor.extractRevisionFromRevCommit(revCommit); + assertThat(revision).isEqualTo("123456abcdef"); + } +} 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 8e43da1d82..da166155a7 100644 Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip differ diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 0897a191a1..4d5d5e8646 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -231,13 +231,13 @@ public class HgFileviewCommand extends AbstractCommand file.setName(getNameFromPath(path)); file.setPath(path); file.setDirectory(false); - file.setLength(stream.decimalIntUpTo(' ')); + file.setLength((long) stream.decimalIntUpTo(' ')); DateTime timestamp = stream.dateTimeUpTo(' '); String description = stream.textUpTo('\0'); if (!disableLastCommit) { - file.setLastModified(timestamp.getDate().getTime()); + file.setCommitDate(timestamp.getDate().getTime()); file.setDescription(description); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index 2116d06a7a..92a05a05a0 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -61,7 +61,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { FileObject file = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); assertEquals("a.txt", file.getName()); assertFalse(file.isDirectory()); - assertTrue(file.getChildren().isEmpty()); + assertTrue(file.getChildren() == null || file.getChildren().isEmpty()); } @Test @@ -73,9 +73,9 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); assertEquals("a.txt", a.getPath()); - assertEquals("added new line for blame", a.getDescription()); - assertTrue(a.getLength() > 0); - checkDate(a.getLastModified()); + assertEquals("added new line for blame", a.getDescription().get()); + assertTrue(a.getLength().getAsLong() > 0); + checkDate(a.getCommitDate().getAsLong()); assertTrue(c.isDirectory()); assertEquals("c", c.getName()); assertEquals("c", c.getPath()); @@ -132,16 +132,16 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertFalse(d.isDirectory()); assertEquals("d.txt", d.getName()); assertEquals("c/d.txt", d.getPath()); - assertEquals("added file d and e in folder c", d.getDescription()); - assertTrue(d.getLength() > 0); - checkDate(d.getLastModified()); + assertEquals("added file d and e in folder c", d.getDescription().get()); + assertTrue(d.getLength().getAsLong() > 0); + checkDate(d.getCommitDate().getAsLong()); assertNotNull(e); assertFalse(e.isDirectory()); assertEquals("e.txt", e.getName()); assertEquals("c/e.txt", e.getPath()); - assertEquals("added file d and e in folder c", e.getDescription()); - assertTrue(e.getLength() > 0); - checkDate(e.getLastModified()); + assertEquals("added file d and e in folder c", e.getDescription().get()); + assertTrue(e.getLength().getAsLong() > 0); + checkDate(e.getCommitDate().getAsLong()); } @Test @@ -154,8 +154,8 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { FileObject a = getFileObject(foList, "a.txt"); - assertNull(a.getDescription()); - assertNull(a.getLastModified()); + assertFalse(a.getDescription().isPresent()); + assertFalse(a.getCommitDate().isPresent()); } @Test diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 99dae0e77b..e4a32c8ca6 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -173,7 +173,7 @@ public class SvnBrowseCommand extends AbstractSvnCommand { if (entry.getDate() != null) { - fileObject.setLastModified(entry.getDate().getTime()); + fileObject.setCommitDate(entry.getDate().getTime()); } fileObject.setDescription(entry.getCommitMessage()); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index d3e6a98558..980d486b5c 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -60,7 +60,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase FileObject file = createCommand().getBrowserResult(request).getFile(); assertEquals("a.txt", file.getName()); assertFalse(file.isDirectory()); - assertTrue(file.getChildren().isEmpty()); + assertTrue(file.getChildren() == null || file.getChildren().isEmpty()); } @Test @@ -73,9 +73,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); assertEquals("a.txt", a.getPath()); - assertEquals("added line for blame test", a.getDescription()); - assertTrue(a.getLength() > 0); - checkDate(a.getLastModified()); + assertEquals("added line for blame test", a.getDescription().get()); + assertTrue(a.getLength().getAsLong() > 0); + checkDate(a.getCommitDate().getAsLong()); assertTrue(c.isDirectory()); assertEquals("c", c.getName()); assertEquals("c", c.getPath()); @@ -122,16 +122,16 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertFalse(d.isDirectory()); assertEquals("d.txt", d.getName()); assertEquals("c/d.txt", d.getPath()); - assertEquals("added d and e in folder c", d.getDescription()); - assertTrue(d.getLength() > 0); - checkDate(d.getLastModified()); + assertEquals("added d and e in folder c", d.getDescription().get()); + assertTrue(d.getLength().getAsLong() > 0); + checkDate(d.getCommitDate().getAsLong()); assertNotNull(e); assertFalse(e.isDirectory()); assertEquals("e.txt", e.getName()); assertEquals("c/e.txt", e.getPath()); - assertEquals("added d and e in folder c", e.getDescription()); - assertTrue(e.getLength() > 0); - checkDate(e.getLastModified()); + assertEquals("added d and e in folder c", e.getDescription().get()); + assertTrue(e.getLength().getAsLong() > 0); + checkDate(e.getCommitDate().getAsLong()); } @Test @@ -144,8 +144,8 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase FileObject a = getFileObject(foList, "a.txt"); - assertNull(a.getDescription()); - assertNull(a.getLastModified()); + assertFalse(a.getDescription().isPresent()); + assertFalse(a.getCommitDate().isPresent()); } @Test diff --git a/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java new file mode 100644 index 0000000000..6c24e72b69 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java @@ -0,0 +1,107 @@ +package sonia.scm.repository.spi; + +import java.io.Closeable; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import java.util.function.Consumer; + +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS; +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONOUS; + +public final class SyncAsyncExecutors { + + public static SyncAsyncExecutor synchronousExecutor() { + return new SyncAsyncExecutor() { + @Override + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { + runnable.accept(SYNCHRONOUS); + return SYNCHRONOUS; + } + + @Override + public boolean hasExecutedAllSynchronously() { + return true; + } + }; + } + + public static SyncAsyncExecutor asynchronousExecutor() { + + Executor executor = Executors.newSingleThreadExecutor(); + + return new SyncAsyncExecutor() { + @Override + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { + executor.execute(() -> runnable.accept(ASYNCHRONOUS)); + return ASYNCHRONOUS; + } + + @Override + public boolean hasExecutedAllSynchronously() { + return true; + } + }; + } + + public static AsyncExecutorStepper stepperAsynchronousExecutor() { + return new AsyncExecutorStepper() { + + Executor executor = Executors.newSingleThreadExecutor(); + Semaphore enterSemaphore = new Semaphore(0); + Semaphore exitSemaphore = new Semaphore(0); + boolean timedOut = false; + + @Override + public void close() { + enterSemaphore.release(Integer.MAX_VALUE/2); + exitSemaphore.release(Integer.MAX_VALUE/2); + } + + @Override + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { + executor.execute(() -> { + try { + enterSemaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (timedOut) { + abortionFallback.run(); + } else { + runnable.accept(ASYNCHRONOUS); + exitSemaphore.release(); + } + }); + return ASYNCHRONOUS; + } + + @Override + public void next() { + enterSemaphore.release(); + try { + exitSemaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void timeout() { + timedOut = true; + close(); + } + + @Override + public boolean hasExecutedAllSynchronously() { + return true; + } + }; + } + + public interface AsyncExecutorStepper extends SyncAsyncExecutor, Closeable { + void next(); + + void timeout(); + } +} diff --git a/scm-ui/eslint-config/package.json b/scm-ui/eslint-config/package.json index b819ad5bd5..ec7216fe5b 100644 --- a/scm-ui/eslint-config/package.json +++ b/scm-ui/eslint-config/package.json @@ -8,8 +8,8 @@ "private": false, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@typescript-eslint/eslint-plugin": "^2.4.0", - "@typescript-eslint/parser": "^2.4.0", + "@typescript-eslint/eslint-plugin": "^2.12.0", + "@typescript-eslint/parser": "^2.12.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-config-prettier": "^6.4.0", diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 93a08a75f2..9bf3c8381f 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -37,8 +37,6 @@ "enzyme-context": "^1.1.2", "enzyme-context-react-router-4": "^2.0.0", "fetch-mock": "^7.5.1", - "flow-bin": "^0.109.0", - "flow-typed": "^2.5.1", "raf": "^3.4.0", "react-test-renderer": "^16.10.2", "storybook-addon-i18next": "^1.2.1", diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 25e78abbd8..7fa1b1193f 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Forms|Checkbox Default 1`] = `