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/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/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-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/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/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/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-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 7bc4c208ee..25e78abbd8 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -2559,6 +2559,8 @@ exports[`Storyshots Table|Table TextColumn 1`] = ` `; +exports[`Storyshots Toast Click to close 1`] = `null`; + exports[`Storyshots Toast Danger 1`] = `null`; exports[`Storyshots Toast Info 1`] = `null`; diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts index da2287f72b..c8990b000c 100644 --- a/scm-ui/ui-components/src/apiclient.ts +++ b/scm-ui/ui-components/src/apiclient.ts @@ -241,7 +241,7 @@ class ApiClient { es.addEventListener(type, listeners[type]); } - return es.close; + return () => es.close(); } } diff --git a/scm-ui/ui-components/src/toast/ToastButton.tsx b/scm-ui/ui-components/src/toast/ToastButton.tsx index 76ae63cd73..24e6cf5d8f 100644 --- a/scm-ui/ui-components/src/toast/ToastButton.tsx +++ b/scm-ui/ui-components/src/toast/ToastButton.tsx @@ -1,12 +1,13 @@ -import React, { FC, useContext } from "react"; +import React, { FC, useContext, MouseEvent } from "react"; import { ToastThemeContext, Themeable } from "./themes"; import styled from "styled-components"; type Props = { icon?: string; + onClick?: () => void; }; -const ThemedButton = styled.div.attrs(props => ({ +const ThemedButton = styled.button.attrs(props => ({ className: "button" }))` color: ${props => props.theme.primary}; @@ -25,10 +26,18 @@ const ToastButtonIcon = styled.i` margin-right: 0.25rem; `; -const ToastButton: FC = ({ icon, children }) => { +const ToastButton: FC = ({ icon, onClick, children }) => { const theme = useContext(ToastThemeContext); + + const handleClick = (e: MouseEvent) => { + if (onClick) { + e.preventDefault(); + onClick(); + } + }; + return ( - + {icon && } {children} ); diff --git a/scm-ui/ui-components/src/toast/index.stories.tsx b/scm-ui/ui-components/src/toast/index.stories.tsx index af17964d2e..01e802a712 100644 --- a/scm-ui/ui-components/src/toast/index.stories.tsx +++ b/scm-ui/ui-components/src/toast/index.stories.tsx @@ -26,7 +26,31 @@ const Animator = () => { ); }; +const Closeable = () => { + const [show, setShow] = useState(true); + + const hide = () => { + setShow(false); + }; + + if (!show) { + return null; + } + + return ( + +

Close the message with a click

+ + + Click to close + + + + ); +}; + toastStories.add("Open/Close", () => ); +toastStories.add("Click to close", () => ); types.forEach(type => { toastStories.add(type.charAt(0).toUpperCase() + type.slice(1), () => ( diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index 4e0e7fe188..dce6947622 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -13,9 +13,11 @@ export type File = { directory: boolean; description?: string; revision: string; - length: number; - lastModified?: string; + length?: number; + commitDate?: string; subRepository?: SubRepository; // TODO + partialResult: boolean; + computationAborted: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/package.json b/scm-ui/ui-webapp/package.json index 0c2b5e4821..ba54fb7689 100644 --- a/scm-ui/ui-webapp/package.json +++ b/scm-ui/ui-webapp/package.json @@ -42,8 +42,6 @@ "@types/styled-components": "^4.1.19", "@types/systemjs": "^0.20.6", "fetch-mock": "^7.5.1", - "flow-bin": "^0.109.0", - "flow-typed": "^2.6.1", "react-test-renderer": "^16.10.2", "redux-mock-store": "^1.5.3" }, diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 3ed9804fe3..7fc9fc3403 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -101,7 +101,9 @@ "length": "Größe", "lastModified": "Zuletzt bearbeitet", "description": "Beschreibung", - "branch": "Branch" + "branch": "Branch", + "notYetComputed": "Noch nicht berechnet; Der Wert wird in Kürze aktualisiert", + "computationAborted": "Die Berechnung dauert zu lange und wurde abgebrochen" }, "content": { "historyButton": "History", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index eaeba49e16..0f5a052256 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -101,7 +101,9 @@ "length": "Length", "lastModified": "Last modified", "description": "Description", - "branch": "Branch" + "branch": "Branch", + "notYetComputed": "Not yet computed, will be updated in a short while", + "computationAborted": "The computation took too long and was aborted" }, "content": { "historyButton": "History", diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index bfcb02e9bb..c1384b5192 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -7,7 +7,7 @@ import styled from "styled-components"; import { binder } from "@scm-manager/ui-extensions"; import { Repository, File } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components"; -import { getFetchSourcesFailure, isFetchSourcesPending, getSources } from "../modules/sources"; +import { getFetchSourcesFailure, isFetchSourcesPending, getSources, fetchSources } from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; type Props = WithTranslation & { @@ -19,10 +19,16 @@ type Props = WithTranslation & { path: string; baseUrl: string; + updateSources: () => void; + // context props match: any; }; +type State = { + stoppableUpdateHandler?: number; +}; + const FixedWidthTh = styled.th` width: 16px; `; @@ -39,7 +45,28 @@ export function findParent(path: string) { return ""; } -class FileTree extends React.Component { +class FileTree extends React.Component { + constructor(props: Props) { + super(props); + this.state = {}; + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { + if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) { + const { tree, updateSources } = this.props; + if (tree?._embedded?.children && tree._embedded.children.find(c => c.partialResult)) { + const stoppableUpdateHandler = setTimeout(updateSources, 3000); + this.setState({ stoppableUpdateHandler: stoppableUpdateHandler }); + } + } + } + + componentWillUnmount(): void { + if (this.state.stoppableUpdateHandler) { + clearTimeout(this.state.stoppableUpdateHandler); + } + } + render() { const { error, loading, tree } = this.props; @@ -106,7 +133,7 @@ class FileTree extends React.Component { {t("sources.file-tree.name")} {t("sources.file-tree.length")} - {t("sources.file-tree.lastModified")} + {t("sources.file-tree.commitDate")} {t("sources.file-tree.description")} {binder.hasExtension("repos.sources.tree.row.right") && } @@ -123,6 +150,14 @@ class FileTree extends React.Component { } } +const mapDispatchToProps = (dispatch: any, ownProps: Props) => { + const { repository, revision, path } = ownProps; + + const updateSources = () => dispatch(fetchSources(repository, revision, path, false)); + + return { updateSources }; +}; + const mapStateToProps = (state: any, ownProps: Props) => { const { repository, revision, path } = ownProps; @@ -141,5 +176,8 @@ const mapStateToProps = (state: any, ownProps: Props) => { export default compose( withRouter, - connect(mapStateToProps) + connect( + mapStateToProps, + mapDispatchToProps + ) )(withTranslation("repos")(FileTree)); diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx index ae92b150ba..cd6baea395 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx @@ -4,10 +4,12 @@ import classNames from "classnames"; import styled from "styled-components"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { File } from "@scm-manager/ui-types"; -import { DateFromNow, FileSize } from "@scm-manager/ui-components"; +import { DateFromNow, FileSize, Tooltip } from "@scm-manager/ui-components"; import FileIcon from "./FileIcon"; +import { Icon } from "@scm-manager/ui-components/src"; +import { WithTranslation, withTranslation } from "react-i18next"; -type Props = { +type Props = WithTranslation & { file: File; baseUrl: string; }; @@ -35,7 +37,7 @@ export function createLink(base: string, file: File) { return link; } -export default class FileTreeLeaf extends React.Component { +class FileTreeLeaf extends React.Component { createLink = (file: File) => { return createLink(this.props.baseUrl, file); }; @@ -62,20 +64,42 @@ export default class FileTreeLeaf extends React.Component { return {file.name}; }; + contentIfPresent = (file: File, attribute: string, content: (file: File) => any) => { + const { t } = this.props; + if (file.hasOwnProperty(attribute)) { + return content(file); + } else if (file.computationAborted) { + return ( + + + + ); + } else if (file.partialResult) { + return ( + + + + ); + } else { + return content(file); + } + }; + render() { const { file } = this.props; - const fileSize = file.directory ? "" : ; + const renderFileSize = (file: File) => ; + const renderCommitDate = (file: File) => ; return ( {this.createFileIcon(file)} {this.createFileName(file)} - {fileSize} - - - - {file.description} + {file.directory ? "" : this.contentIfPresent(file, "length", renderFileSize)} + {this.contentIfPresent(file, "commitDate", renderCommitDate)} + + {this.contentIfPresent(file, "description", file => file.description)} + {binder.hasExtension("repos.sources.tree.row.right") && ( {!file.directory && ( @@ -93,3 +117,5 @@ export default class FileTreeLeaf extends React.Component { ); } } + +export default withTranslation("repos")(FileTreeLeaf); diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx index adb56ecd97..34c07e0be1 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Content.tsx @@ -115,7 +115,7 @@ class Content extends React.Component { showMoreInformation() { const collapsed = this.state.collapsed; const { file, revision, t, repository } = this.props; - const date = ; + const date = ; const description = file.description ? (

{file.description.split("\n").map((item, key) => { diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index 55737f7ee0..2d05c5814e 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -49,10 +49,8 @@ const collection = { name: "src", path: "src", directory: true, - description: "", length: 176, revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4", - lastModified: "", subRepository: undefined, _links: { self: { @@ -71,7 +69,7 @@ const collection = { description: "bump version", length: 780, revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4", - lastModified: "2017-07-31T11:17:19Z", + commitDate: "2017-07-31T11:17:19Z", subRepository: undefined, _links: { self: { @@ -127,7 +125,7 @@ describe("sources fetch", () => { { type: FETCH_SOURCES_SUCCESS, itemId: "scm/core/_/", - payload: collection + payload: { updatePending: false, sources: collection } } ]; @@ -148,7 +146,7 @@ describe("sources fetch", () => { { type: FETCH_SOURCES_SUCCESS, itemId: "scm/core/abc/src", - payload: collection + payload: { updatePending: false, sources: collection } } ]; @@ -182,14 +180,14 @@ describe("reducer tests", () => { it("should store the collection, without revision and path", () => { const expectedState = { - "scm/core/_/": collection + "scm/core/_/": { updatePending: false, sources: collection } }; expect(reducer({}, fetchSourcesSuccess(repository, "", "", collection))).toEqual(expectedState); }); it("should store the collection, with revision and path", () => { const expectedState = { - "scm/core/abc/src/main": collection + "scm/core/abc/src/main": { updatePending: false, sources: collection } }; expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", collection))).toEqual(expectedState); }); @@ -200,7 +198,7 @@ describe("selector tests", () => { const state = { sources: { "scm/core/abc/src/main/package.json": { - noDirectory + sources: {noDirectory} } } }; @@ -223,7 +221,9 @@ describe("selector tests", () => { it("should return the source collection without revision and path", () => { const state = { sources: { - "scm/core/_/": collection + "scm/core/_/": { + sources: collection + } } }; expect(getSources(state, repository, "", "")).toBe(collection); @@ -232,7 +232,9 @@ describe("selector tests", () => { it("should return the source collection with revision and path", () => { const state = { sources: { - "scm/core/abc/src/main": collection + "scm/core/abc/src/main": { + sources: collection + } } }; expect(getSources(state, repository, "abc", "src/main")).toBe(collection); diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 826b32f1f6..8abdec9fd5 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -9,13 +9,25 @@ export const FETCH_SOURCES_PENDING = `${FETCH_SOURCES}_${types.PENDING_SUFFIX}`; export const FETCH_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`; export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`; -export function fetchSources(repository: Repository, revision: string, path: string) { - return function(dispatch: any) { - dispatch(fetchSourcesPending(repository, revision, path)); +export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true) { + return function(dispatch: any, getState: () => any) { + const state = getState(); + if ( + isFetchSourcesPending(state, repository, revision, path) || + isUpdateSourcePending(state, repository, revision, path) + ) { + return; + } + + if (initialLoad) { + dispatch(fetchSourcesPending(repository, revision, path)); + } else { + dispatch(updateSourcesPending(repository, revision, path, getSources(state, repository, revision, path))); + } return apiClient .get(createUrl(repository, revision, path)) .then(response => response.json()) - .then(sources => { + .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, sources)); }) .catch(err => { @@ -42,10 +54,23 @@ export function fetchSourcesPending(repository: Repository, revision: string, pa }; } +export function updateSourcesPending( + repository: Repository, + revision: string, + path: string, + currentSources: any +): Action { + return { + type: "UPDATE_PENDING", + payload: { updatePending: true, sources: currentSources }, + itemId: createItemId(repository, revision, path) + }; +} + export function fetchSourcesSuccess(repository: Repository, revision: string, path: string, sources: File) { return { type: FETCH_SOURCES_SUCCESS, - payload: sources, + payload: { updatePending: false, sources }, itemId: createItemId(repository, revision, path) }; } @@ -72,7 +97,7 @@ export default function reducer( type: "UNKNOWN" } ): any { - if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) { + if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === "UPDATE_PENDING")) { return { ...state, [action.itemId]: action.payload @@ -99,13 +124,17 @@ export function getSources( path: string ): File | null | undefined { if (state.sources) { - return state.sources[createItemId(repository, revision, path)]; + return state.sources[createItemId(repository, revision, path)]?.sources; } return null; } export function isFetchSourcesPending(state: any, repository: Repository, revision: string, path: string): boolean { - return isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); + return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); +} + +function isUpdateSourcePending(state: any, repository: Repository, revision: string, path: string): boolean { + return state?.sources && state.sources[createItemId(repository, revision, path)]?.updatePending; } export function getFetchSourcesFailure( diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java index b8e34f8101..f9304881e7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import com.google.common.annotations.VisibleForTesting; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.mapstruct.Context; @@ -16,18 +15,13 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.time.Instant; +import java.util.Optional; +import java.util.OptionalLong; @Mapper public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectDtoMapper { - @Inject - private FileObjectToFileObjectDtoMapper childrenMapper; - - @VisibleForTesting - void setChildrenMapper(FileObjectToFileObjectDtoMapper childrenMapper) { - this.childrenMapper = childrenMapper; - } - FileObjectDto map(BrowserResult browserResult, @Context NamespaceAndName namespaceAndName) { FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult); fileObjectDto.setRevision(browserResult.getRevision()); @@ -36,12 +30,8 @@ public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectD @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "children", qualifiedBy = Children.class) - protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult); - @Children - protected FileObjectDto childrenToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult) { - return childrenMapper.map(fileObject, namespaceAndName, browserResult); - } + protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult); @Override void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) { @@ -52,6 +42,14 @@ public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectD applyEnrichers(appender, fileObject, namespaceAndName, browserResult, browserResult.getRevision()); } + Optional mapOptionalInstant(OptionalLong optionalLong) { + if (optionalLong.isPresent()) { + return Optional.of(Instant.ofEpochMilli(optionalLong.getAsLong())); + } else { + return Optional.empty(); + } + } + @Qualifier @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java index 0bce564e35..b273f241dc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java @@ -10,6 +10,8 @@ import lombok.Setter; import java.time.Instant; import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; @Getter @Setter @@ -19,14 +21,16 @@ public class FileObjectDto extends HalRepresentation { private String path; private boolean directory; @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String description; - private long length; + private Optional description; + private OptionalLong length; @JsonInclude(JsonInclude.Include.NON_EMPTY) - private Instant lastModified; + private Optional commitDate; @JsonInclude(JsonInclude.Include.NON_EMPTY) private SubRepositoryDto subRepository; @JsonInclude(JsonInclude.Include.NON_EMPTY) private String revision; + private boolean partialResult; + private boolean computationAborted; public FileObjectDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java deleted file mode 100644 index c2884a460f..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package sonia.scm.api.v2.resources; - -import de.otto.edison.hal.Embedded; -import de.otto.edison.hal.Links; -import org.mapstruct.Context; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import sonia.scm.repository.BrowserResult; -import sonia.scm.repository.FileObject; -import sonia.scm.repository.NamespaceAndName; - -@Mapper -public abstract class FileObjectToFileObjectDtoMapper extends BaseFileObjectDtoMapper { - - @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes - protected abstract FileObjectDto map(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult); - - @Override - void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) { - applyEnrichers(new EdisonHalAppender(links, embeddedBuilder), fileObject, namespaceAndName, browserResult, browserResult.getRevision()); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 48de63969d..c7d88e7c0a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -37,7 +37,6 @@ public class MapperModule extends AbstractModule { bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass()); - bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass()); bind(BrowserResultToFileObjectDtoMapper.class).to(Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class).getClass()); bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass()); diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java index 62793da0f6..d5ccb73c7d 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java @@ -12,6 +12,7 @@ import sonia.scm.debug.DebugModule; import sonia.scm.filter.WebElementModule; import sonia.scm.plugin.ExtensionProcessor; import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.ExecutorModule; import javax.servlet.ServletContext; import java.util.ArrayList; @@ -51,6 +52,7 @@ public class ApplicationModuleProvider implements ModuleProvider { moduleList.add(new DebugModule()); } moduleList.add(new MapperModule()); + moduleList.add(new ExecutorModule()); return moduleList; } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java new file mode 100644 index 0000000000..de9e27ecda --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java @@ -0,0 +1,49 @@ +package sonia.scm.repository; + +import sonia.scm.repository.spi.SyncAsyncExecutor; + +import java.time.Instant; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS; +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONOUS; + +public class DefaultSyncAsyncExecutor implements SyncAsyncExecutor { + + private final Executor executor; + private final Instant switchToAsyncTime; + private final long maxAsyncAbortMilliseconds; + private AtomicLong accumulatedAsyncRuntime = new AtomicLong(0L); + private boolean executedAllSynchronously = true; + + DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime, int maxAsyncAbortSeconds) { + this.executor = executor; + this.switchToAsyncTime = switchToAsyncTime; + this.maxAsyncAbortMilliseconds = maxAsyncAbortSeconds * 1000L; + } + + public ExecutionType execute(Consumer task, Runnable abortionFallback) { + if (switchToAsyncTime.isBefore(Instant.now())) { + executor.execute(() -> { + if (accumulatedAsyncRuntime.get() < maxAsyncAbortMilliseconds) { + long chunkStartTime = System.currentTimeMillis(); + task.accept(ASYNCHRONOUS); + accumulatedAsyncRuntime.addAndGet(System.currentTimeMillis() - chunkStartTime); + } else { + abortionFallback.run(); + } + }); + executedAllSynchronously = false; + return ASYNCHRONOUS; + } else { + task.accept(SYNCHRONOUS); + return SYNCHRONOUS; + } + } + + public boolean hasExecutedAllSynchronously() { + return executedAllSynchronously; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java new file mode 100644 index 0000000000..1727fff970 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java @@ -0,0 +1,55 @@ +package sonia.scm.repository; + +import sonia.scm.repository.spi.SyncAsyncExecutor; +import sonia.scm.repository.spi.SyncAsyncExecutorProvider; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.Closeable; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Singleton +public class DefaultSyncAsyncExecutorProvider implements SyncAsyncExecutorProvider, Closeable { + + public static final int DEFAULT_MAX_ASYNC_ABORT_SECONDS = 60; + public static final String MAX_ASYNC_ABORT_SECONDS_PROPERTY = "scm.maxAsyncAbortSeconds"; + + public static final int DEFAULT_NUMBER_OF_THREADS = 4; + public static final String NUMBER_OF_THREADS_PROPERTY = "scm.asyncThreads"; + + private final ExecutorService executor; + private final int defaultMaxAsyncAbortSeconds; + + @Inject + public DefaultSyncAsyncExecutorProvider() { + this(Executors.newFixedThreadPool(getProperty(NUMBER_OF_THREADS_PROPERTY, DEFAULT_NUMBER_OF_THREADS))); + } + + public DefaultSyncAsyncExecutorProvider(ExecutorService executor) { + this.executor = executor; + this.defaultMaxAsyncAbortSeconds = getProperty(MAX_ASYNC_ABORT_SECONDS_PROPERTY, DEFAULT_MAX_ASYNC_ABORT_SECONDS); + } + + public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds) { + return createExecutorWithSecondsToTimeout(switchToAsyncInSeconds, defaultMaxAsyncAbortSeconds); + } + + public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds, int maxAsyncAbortSeconds) { + return new DefaultSyncAsyncExecutor( + executor, + Instant.now().plus(switchToAsyncInSeconds, ChronoUnit.SECONDS), + maxAsyncAbortSeconds); + } + + @Override + public void close() { + executor.shutdownNow(); + } + + private static int getProperty(String key, int defaultValue) { + return Integer.parseInt(System.getProperty(key, Integer.toString(defaultValue))); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/ExecutorModule.java b/scm-webapp/src/main/java/sonia/scm/repository/ExecutorModule.java new file mode 100644 index 0000000000..4e494b6ba6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/ExecutorModule.java @@ -0,0 +1,12 @@ +package sonia.scm.repository; + +import com.google.inject.AbstractModule; +import sonia.scm.lifecycle.modules.CloseableModule; +import sonia.scm.repository.spi.SyncAsyncExecutorProvider; + +public class ExecutorModule extends AbstractModule { + @Override + protected void configure() { + bind(SyncAsyncExecutorProvider.class).to(DefaultSyncAsyncExecutorProvider.class); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java index 162a90d45a..273cc25018 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java @@ -8,7 +8,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mapstruct.factory.Mappers; -import org.mockito.InjectMocks; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; @@ -24,9 +23,6 @@ public class BrowserResultToFileObjectDtoMapperTest { private final URI baseUri = URI.create("http://example.com/base/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - @InjectMocks - private FileObjectToFileObjectDtoMapperImpl fileObjectToFileObjectDtoMapper; - private BrowserResultToFileObjectDtoMapper mapper; private final Subject subject = mock(Subject.class); @@ -34,28 +30,28 @@ public class BrowserResultToFileObjectDtoMapperTest { private FileObject fileObject1 = new FileObject(); private FileObject fileObject2 = new FileObject(); + private FileObject partialFileObject = new FileObject(); @Before public void init() { initMocks(this); mapper = Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class); - mapper.setChildrenMapper(fileObjectToFileObjectDtoMapper); mapper.setResourceLinks(resourceLinks); subjectThreadState.bind(); ThreadContext.bind(subject); fileObject1.setName("FO 1"); - fileObject1.setLength(100); - fileObject1.setLastModified(0L); + fileObject1.setLength(100L); + fileObject1.setCommitDate(0L); fileObject1.setPath("/path/object/1"); fileObject1.setDescription("description of file object 1"); fileObject1.setDirectory(false); fileObject2.setName("FO 2"); - fileObject2.setLength(100); - fileObject2.setLastModified(101L); + fileObject2.setLength(100L); + fileObject2.setCommitDate(101L); fileObject2.setPath("/path/object/2"); fileObject2.setDescription("description of file object 2"); fileObject2.setDirectory(true); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java deleted file mode 100644 index de357848c5..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package sonia.scm.api.v2.resources; - -import org.apache.shiro.subject.Subject; -import org.apache.shiro.subject.support.SubjectThreadState; -import org.apache.shiro.util.ThreadContext; -import org.apache.shiro.util.ThreadState; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.repository.BrowserResult; -import sonia.scm.repository.FileObject; -import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.SubRepository; - -import java.net.URI; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -@RunWith(MockitoJUnitRunner.Silent.class) -public class FileObjectToFileObjectDtoMapperTest { - - private final URI baseUri = URI.create("http://example.com/base/"); - @SuppressWarnings("unused") // Is injected - private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - - @InjectMocks - private FileObjectToFileObjectDtoMapperImpl mapper; - - private final Subject subject = mock(Subject.class); - private final ThreadState subjectThreadState = new SubjectThreadState(subject); - - private URI expectedBaseUri; - - @Before - public void init() { - expectedBaseUri = baseUri.resolve(RepositoryRootResource.REPOSITORIES_PATH_V2 + "/"); - subjectThreadState.bind(); - ThreadContext.bind(subject); - } - - @After - public void unbind() { - ThreadContext.unbindSubject(); - } - - @Test - public void shouldMapAttributesCorrectly() { - FileObject fileObject = createFileObject(); - FileObjectDto dto = mapper.map(fileObject, new NamespaceAndName("namespace", "name"), new BrowserResult("revision", fileObject)); - - assertEqualAttributes(fileObject, dto); - } - - @Test - public void shouldHaveCorrectSelfLinkForDirectory() { - FileObject fileObject = createDirectoryObject(); - FileObjectDto dto = mapper.map(fileObject, new NamespaceAndName("namespace", "name"), new BrowserResult("revision", fileObject)); - - assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo(expectedBaseUri.resolve("namespace/name/sources/revision/foo/bar").toString()); - } - - @Test - public void shouldHaveCorrectContentLink() { - FileObject fileObject = createFileObject(); - fileObject.setDirectory(false); - FileObjectDto dto = mapper.map(fileObject, new NamespaceAndName("namespace", "name"), new BrowserResult("revision", fileObject)); - - assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo(expectedBaseUri.resolve("namespace/name/content/revision/foo/bar").toString()); - } - - @Test - public void shouldAppendLinks() { - HalEnricherRegistry registry = new HalEnricherRegistry(); - registry.register(FileObject.class, (ctx, appender) -> { - NamespaceAndName repository = ctx.oneRequireByType(NamespaceAndName.class); - FileObject fo = ctx.oneRequireByType(FileObject.class); - String rev = ctx.oneRequireByType(String.class); - - appender.appendLink("hog", "http://" + repository.logString() + "/" + fo.getName() + "/" + rev); - }); - mapper.setRegistry(registry); - - FileObject fileObject = createFileObject(); - FileObjectDto dto = mapper.map(fileObject, new NamespaceAndName("hitchhiker", "hog"), new BrowserResult("42", fileObject)); - - assertThat(dto.getLinks().getLinkBy("hog").get().getHref()).isEqualTo("http://hitchhiker/hog/foo/42"); - } - - private FileObject createDirectoryObject() { - FileObject fileObject = createFileObject(); - fileObject.setDirectory(true); - return fileObject; - } - - private FileObject createFileObject() { - FileObject fileObject = new FileObject(); - fileObject.setName("foo"); - fileObject.setDescription("bar"); - fileObject.setPath("foo/bar"); - fileObject.setDirectory(false); - fileObject.setLength(100); - fileObject.setLastModified(123L); - - fileObject.setSubRepository(new SubRepository("repo.url")); - return fileObject; - } - - private void assertEqualAttributes(FileObject fileObject, FileObjectDto dto) { - assertThat(dto.getName()).isEqualTo(fileObject.getName()); - assertThat(dto.getDescription()).isEqualTo(fileObject.getDescription()); - assertThat(dto.getPath()).isEqualTo(fileObject.getPath()); - assertThat(dto.isDirectory()).isEqualTo(fileObject.isDirectory()); - assertThat(dto.getLength()).isEqualTo(fileObject.getLength()); - assertThat(dto.getLastModified().toEpochMilli()).isEqualTo((long) fileObject.getLastModified()); - assertThat(dto.getSubRepository().getBrowserUrl()).isEqualTo(fileObject.getSubRepository().getBrowserUrl()); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 37c7659b1c..0023ea4556 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -7,7 +7,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mapstruct.factory.Mappers; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.NotFoundException; @@ -41,16 +40,12 @@ public class SourceRootResourceTest extends RepositoryTestBase { @Mock private BrowseCommandBuilder browseCommandBuilder; - @InjectMocks - private FileObjectToFileObjectDtoMapperImpl fileObjectToFileObjectDtoMapper; - private BrowserResultToFileObjectDtoMapper browserResultToFileObjectDtoMapper; @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { browserResultToFileObjectDtoMapper = Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class); - browserResultToFileObjectDtoMapper.setChildrenMapper(fileObjectToFileObjectDtoMapper); browserResultToFileObjectDtoMapper.setResourceLinks(resourceLinks); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(service.getBrowseCommand()).thenReturn(browseCommandBuilder); @@ -127,7 +122,7 @@ public class SourceRootResourceTest extends RepositoryTestBase { fileObject1.setDescription("File object 1"); fileObject1.setPath("/foo/bar/fo1"); fileObject1.setLength(1024L); - fileObject1.setLastModified(0L); + fileObject1.setCommitDate(0L); parent.addChild(fileObject1); FileObject fileObject2 = new FileObject(); @@ -136,7 +131,7 @@ public class SourceRootResourceTest extends RepositoryTestBase { fileObject2.setDescription("File object 2"); fileObject2.setPath("/foo/bar/fo2"); fileObject2.setLength(4096L); - fileObject2.setLastModified(1234L); + fileObject2.setCommitDate(1234L); parent.addChild(fileObject2); return parent; diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java new file mode 100644 index 0000000000..7e4ad60157 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java @@ -0,0 +1,54 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType; + +import java.time.Instant; + +import static java.lang.Integer.MAX_VALUE; +import static java.time.Instant.MAX; +import static java.time.temporal.ChronoUnit.MILLIS; +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS; +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONOUS; + +class DefaultSyncAsyncExecutorTest { + + ExecutionType calledWithType = null; + boolean aborted = false; + + @Test + void shouldExecuteSynchronouslyBeforeTimeout() { + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, MAX, MAX_VALUE); + + ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); + + assertThat(result).isEqualTo(SYNCHRONOUS); + assertThat(calledWithType).isEqualTo(SYNCHRONOUS); + assertThat(executor.hasExecutedAllSynchronously()).isTrue(); + assertThat(aborted).isFalse(); + } + + @Test + void shouldExecuteAsynchronouslyAfterTimeout() { + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS), MAX_VALUE); + + ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); + + assertThat(result).isEqualTo(ASYNCHRONOUS); + assertThat(calledWithType).isEqualTo(ASYNCHRONOUS); + assertThat(executor.hasExecutedAllSynchronously()).isFalse(); + assertThat(aborted).isFalse(); + } + + @Test + void shouldCallFallbackAfterAbortion() { + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS), 0); + + ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); + + assertThat(result).isEqualTo(ASYNCHRONOUS); + assertThat(calledWithType).isNull(); + assertThat(aborted).isTrue(); + } +} diff --git a/yarn.lock b/yarn.lock index c252f3c3f1..40eaddf70c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -840,7 +840,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.7.4" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.6.0": +"@babel/polyfill@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc" integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw== @@ -2238,7 +2238,7 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^16.28.4", "@octokit/rest@^16.33.1": +"@octokit/rest@^16.28.4": version "16.34.0" resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.34.0.tgz#8703e46d7e9f6aec24a7e591b073f325ca13f6e2" integrity sha512-EBe5qMQQOZRuezahWCXCnSe0J6tAqrW2hrEH9U8esXzKor1+HUDf8jgImaZf5lkTyWCQA296x9kAH5c0pxEgVQ== @@ -2274,11 +2274,6 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" -"@sindresorhus/is@^0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" - integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== - "@storybook/addon-actions@^5.2.3": version "5.2.8" resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.2.8.tgz#f63c6e1afb59e94ca1ebc776b7cad9b815e7419e" @@ -3177,46 +3172,48 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^2.4.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.6.0.tgz#e82ed43fc4527b21bfe35c20a2d6e4ed49fc7957" - integrity sha512-iCcXREU4RciLmLniwKLRPCOFVXrkF7z27XuHq5DrykpREv/mz6ztKAyLg2fdkM0hQC7659p5ZF5uStH7uzAJ/w== +"@typescript-eslint/eslint-plugin@^2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.12.0.tgz#0da7cbca7b24f4c6919e9eb31c704bfb126f90ad" + integrity sha512-1t4r9rpLuEwl3hgt90jY18wJHSyb0E3orVL3DaqwmpiSDHmHiSspVsvsFF78BJ/3NNG3qmeso836jpuBWYziAA== dependencies: - "@typescript-eslint/experimental-utils" "2.6.0" - eslint-utils "^1.4.2" + "@typescript-eslint/experimental-utils" "2.12.0" + eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" - regexpp "^2.0.1" + regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.6.0.tgz#ed70bef72822bff54031ff0615fc888b9e2b6e8a" - integrity sha512-34BAFpNOwHXeqT+AvdalLxOvcPYnCxA5JGmBAFL64RGMdP0u65rXjii7l/nwpgk5aLEE1LaqF+SsCU0/Cb64xA== +"@typescript-eslint/experimental-utils@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.12.0.tgz#e0a76ffb6293e058748408a191921e453c31d40d" + integrity sha512-jv4gYpw5N5BrWF3ntROvCuLe1IjRenLy5+U57J24NbPGwZFAjhnM45qpq0nDH1y/AZMb3Br25YiNVwyPbz6RkA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.6.0" + "@typescript-eslint/typescript-estree" "2.12.0" eslint-scope "^5.0.0" -"@typescript-eslint/parser@^2.4.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.6.0.tgz#5106295c6a7056287b4719e24aae8d6293d5af49" - integrity sha512-AvLejMmkcjRTJ2KD72v565W4slSrrzUIzkReu1JN34b8JnsEsxx7S9Xx/qXEuMQas0mkdUfETr0j3zOhq2DIqQ== +"@typescript-eslint/parser@^2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.12.0.tgz#393f1604943a4ca570bb1a45bc8834e9b9158884" + integrity sha512-lPdkwpdzxEfjI8TyTzZqPatkrswLSVu4bqUgnB03fHSOwpC7KSerPgJRgIAf11UGNf7HKjJV6oaPZI4AghLU6g== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.6.0" - "@typescript-eslint/typescript-estree" "2.6.0" + "@typescript-eslint/experimental-utils" "2.12.0" + "@typescript-eslint/typescript-estree" "2.12.0" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/typescript-estree@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.6.0.tgz#d3e9d8e001492e2b9124c4d4bd4e7f03c0fd7254" - integrity sha512-A3lSBVIdj2Gp0lFEL6in2eSPqJ33uAc3Ko+Y4brhjkxzjbzLnwBH22CwsW2sCo+iwogfIyvb56/AJri15H0u5Q== +"@typescript-eslint/typescript-estree@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.12.0.tgz#bd9e547ccffd17dfab0c3ab0947c80c8e2eb914c" + integrity sha512-rGehVfjHEn8Frh9UW02ZZIfJs6SIIxIu/K1bbci8rFfDE/1lQ8krIJy5OXOV3DVnNdDPtoiPOdEANkLMrwXbiQ== dependencies: debug "^4.1.1" - glob "^7.1.4" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" is-glob "^4.0.1" lodash.unescape "4.0.1" semver "^6.3.0" + tsutils "^3.17.1" "@webassemblyjs/ast@1.8.5": version "1.8.5" @@ -4380,11 +4377,6 @@ before-after-hook@^2.0.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== -big-integer@^1.6.17: - version "1.6.47" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.47.tgz#e1e9320e26c4cc81f64fbf4b3bb20e025bf18e2d" - integrity sha512-9t9f7X3as2XGX8b52GqG6ox0GvIdM86LyIXASJnDCFhYNgt+A+MByQZ3W2PyMRZjEvG5f8TEbSPfEotVuMJnQg== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -4395,14 +4387,6 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== -binary@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" - integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= - dependencies: - buffers "~0.1.1" - chainsaw "~0.1.0" - block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -4420,11 +4404,6 @@ bluebird@^3.5.1, bluebird@^3.5.3: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de" integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== -bluebird@~3.4.1: - version "3.4.7" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" - integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM= - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -4621,11 +4600,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer-indexof-polyfill@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz#a9fb806ce8145d5428510ce72f278bb363a638bf" - integrity sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8= - buffer-indexof@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" @@ -4650,11 +4624,6 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffers@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" - integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= - builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -4748,19 +4717,6 @@ cache-loader@^4.1.0: neo-async "^2.6.1" schema-utils "^2.0.0" -cacheable-request@^2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" - integrity sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0= - dependencies: - clone-response "1.0.2" - get-stream "3.0.0" - http-cache-semantics "3.8.1" - keyv "3.0.0" - lowercase-keys "1.0.0" - normalize-url "2.0.1" - responselike "1.0.2" - call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -4882,13 +4838,6 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chainsaw@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" - integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= - dependencies: - traverse ">=0.3.0 <0.4" - chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -4929,11 +4878,6 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - cheerio@^1.0.0-rc.2: version "1.0.0-rc.3" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" @@ -5102,13 +5046,6 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clone-response@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -5179,7 +5116,7 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" -colors@^1.1.2, colors@^1.3.2: +colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -5623,11 +5560,6 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -5959,13 +5891,6 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -6387,18 +6312,6 @@ dotenv@^8.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== -duplexer2@~0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= - dependencies: - readable-stream "^2.0.2" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" @@ -6844,7 +6757,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.2, eslint-utils@^1.4.3: +eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== @@ -7380,49 +7293,6 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== -flow-bin@^0.109.0: - version "0.109.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.109.0.tgz#dcdcb7402dd85b58200392d8716ccf14e5a8c24c" - integrity sha512-tpcMTpAGIRivYhFV3KJq+zHI2HzcXo8MoGe9pXS4G/UZuey2Faq/e8/gdph2WF0erRlML5hmwfwiq7v9c25c7w== - -flow-typed@^2.5.1, flow-typed@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/flow-typed/-/flow-typed-2.6.2.tgz#6d324a96c4df300e0f823c13ca879c824bef40ce" - integrity sha512-brTh8SukLidVpR1u8hSR3OcZSvLtptpwLEGgEhK/qBhWCB7zxPZmnmChYi40JQH6vB448ck380+qbkDo3fJ6qA== - dependencies: - "@babel/polyfill" "^7.0.0" - "@octokit/rest" "^16.33.1" - colors "^1.3.2" - flowgen "^1.9.0" - fs-extra "^7.0.0" - glob "^7.1.3" - got "^8.3.2" - md5 "^2.2.1" - mkdirp "^0.5.1" - prettier "^1.18.2" - rimraf "^2.6.2" - semver "^5.5.1" - table "^5.0.2" - through "^2.3.8" - unzipper "^0.9.3" - which "^1.3.1" - yargs "^12.0.2" - -flowgen@^1.9.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/flowgen/-/flowgen-1.10.0.tgz#a041ae31d543d22166e7ba7c296b8445deb3c2e4" - integrity sha512-3lsoaa1vxGXhnkHuoE4mLPJi/klvpR3ID8R9CFJ/GBNi+cxJXecWQaUPrWMdNI5tGs8Y+7wrIZaCVFKFLQiGOg== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/highlight" "^7.0.0" - commander "^2.11.0" - lodash "^4.17.4" - paralleljs "^0.2.1" - prettier "^1.16.4" - shelljs "^0.8.3" - typescript "^3.4" - typescript-compiler "^1.4.1-2" - flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -7510,7 +7380,7 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -from2@^2.1.0, from2@^2.1.1: +from2@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= @@ -7529,15 +7399,6 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^8.0.1, fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -7679,7 +7540,7 @@ get-stdin@^6.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== -get-stream@3.0.0, get-stream@^3.0.0: +get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= @@ -7781,7 +7642,7 @@ glob-to-regexp@^0.4.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: +glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -7917,29 +7778,6 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" -got@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" - integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== - dependencies: - "@sindresorhus/is" "^0.7.0" - cacheable-request "^2.1.1" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - into-stream "^3.1.0" - is-retry-allowed "^1.1.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - mimic-response "^1.0.0" - p-cancelable "^0.4.0" - p-timeout "^2.0.1" - pify "^3.0.0" - safe-buffer "^5.1.1" - timed-out "^4.0.1" - url-parse-lax "^3.0.0" - url-to-options "^1.0.1" - graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" @@ -8004,23 +7842,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= -has-symbol-support-x@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" - integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== - has-symbols@^1.0.0, has-symbols@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== -has-to-string-tag-x@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" - integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== - dependencies: - has-symbol-support-x "^1.4.1" - has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -8270,7 +8096,7 @@ htmlparser2@^4.0: domutils "^2.0.0" entities "^2.0.0" -http-cache-semantics@3.8.1, http-cache-semantics@^3.8.1: +http-cache-semantics@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== @@ -8635,14 +8461,6 @@ interpret@1.2.0, interpret@^1.0.0, interpret@^1.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== -into-stream@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" - integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY= - dependencies: - from2 "^2.1.1" - p-is-promise "^1.1.0" - invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -8744,7 +8562,7 @@ is-boolean-object@^1.0.0: resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M= -is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1: +is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -8975,11 +8793,6 @@ is-resolvable@^1.0.0: resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== -is-retry-allowed@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - is-root@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" @@ -9158,14 +8971,6 @@ istanbul-reports@^2.2.6: dependencies: handlebars "^4.1.2" -isurl@^1.0.0-alpha5: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" - integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== - dependencies: - has-to-string-tag-x "^1.2.0" - is-object "^1.0.1" - jest-changed-files@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" @@ -9612,11 +9417,6 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -9698,13 +9498,6 @@ jsx-ast-utils@^2.2.1: array-includes "^3.0.3" object.assign "^4.1.0" -keyv@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" - integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== - dependencies: - json-buffer "3.0.0" - killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -9847,11 +9640,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -listenercount@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" - integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc= - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -10046,7 +9834,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.10: +lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.10: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -10076,16 +9864,6 @@ lower-case@^1.1.1: resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= -lowercase-keys@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" - integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= - -lowercase-keys@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - lowlight@~1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc" @@ -10229,15 +10007,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -md5@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - mdast-add-list-metadata@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz#95e73640ce2fc1fa2dcb7ec443d09e2bfe7db4cf" @@ -10435,11 +10204,6 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -10954,15 +10718,6 @@ normalize-url@1.9.1: query-string "^4.1.0" sort-keys "^1.0.0" -normalize-url@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" - integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== - dependencies: - prepend-http "^2.0.0" - query-string "^5.0.1" - sort-keys "^2.0.0" - normalize-url@^3.0.0, normalize-url@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" @@ -11303,11 +11058,6 @@ osenv@0, osenv@^0.1.4, osenv@^0.1.5: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -p-cancelable@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" - integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== - p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -11325,11 +11075,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-is-promise@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" - integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= - p-is-promise@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" @@ -11406,13 +11151,6 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" -p-timeout@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" - integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== - dependencies: - p-finally "^1.0.0" - p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -11444,11 +11182,6 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" -paralleljs@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/paralleljs/-/paralleljs-0.2.1.tgz#ebca745d3e09c01e2bebcc14858891ff4510e926" - integrity sha1-68p0XT4JwB4r68wUhYiR/0UQ6SY= - param-case@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" @@ -12141,11 +11874,6 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -12158,11 +11886,6 @@ prettier-reflow@^1.18.2-1: resolved "https://registry.yarnpkg.com/prettier-reflow/-/prettier-reflow-1.18.2-2.tgz#8959aabf7b23138fa85b54a26f65afd15a52cfde" integrity sha512-Pd/rr0Si0f5qPQM+OheBsGV8w/u1mjpfXZPyVxLieG3aOLIOhbwzeCkoCPIlQ3VefWwKDq6gijoPlsa/fHQlFw== -prettier@^1.16.4, prettier@^1.18.2: - version "1.18.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" - integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== - prettier@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" @@ -13191,6 +12914,11 @@ regexpp@^2.0.1: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== +regexpp@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e" + integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g== + regexpu-core@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6" @@ -13405,13 +13133,6 @@ resolve@^1.12.0, resolve@^1.5.0: dependencies: path-parse "^1.0.6" -responselike@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -13738,7 +13459,7 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@^1.0.5, setimmediate@~1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -14521,7 +14242,7 @@ systemjs@0.21.6: resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-0.21.6.tgz#9d15e79d9f60abbac23f0d179f887ec01f260a1b" integrity sha512-R+5S9eV9vcQgWOoS4D87joZ4xkFJHb19ZsyKY07D1+VBDE9bwYcU+KXE0r5XlDA8mFoJGyuWDbfrNoh90JsA8g== -table@^5.0.2, table@^5.2.3: +table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== @@ -14688,7 +14409,7 @@ through2@^3.0.0: dependencies: readable-stream "2 || 3" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -14698,11 +14419,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -timed-out@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= - timers-browserify@^2.0.4: version "2.0.11" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" @@ -14817,11 +14533,6 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -"traverse@>=0.3.0 <0.4": - version "0.3.9" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" - integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= - trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -14938,16 +14649,6 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript-compiler@^1.4.1-2: - version "1.4.1-2" - resolved "https://registry.yarnpkg.com/typescript-compiler/-/typescript-compiler-1.4.1-2.tgz#ba4f7db22d91534a1929d90009dce161eb72fd3f" - integrity sha1-uk99si2RU0oZKdkACdzhYety/T8= - -typescript@^3.4: - version "3.6.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d" - integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg== - typescript@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" @@ -15124,21 +14825,6 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -unzipper@^0.9.3: - version "0.9.15" - resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.9.15.tgz#97d99203dad17698ee39882483c14e4845c7549c" - integrity sha512-2aaUvO4RAeHDvOCuEtth7jrHFaCKTSXPqUkXwADaLBzGbgZGzUDccoEdJ5lW+3RmfpOZYNx0Rw6F6PUzM6caIA== - dependencies: - big-integer "^1.6.17" - binary "~0.3.0" - bluebird "~3.4.1" - buffer-indexof-polyfill "~1.0.0" - duplexer2 "~0.1.4" - fstream "^1.0.12" - listenercount "~1.0.1" - readable-stream "~2.3.6" - setimmediate "~1.0.4" - upath@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" @@ -15170,13 +14856,6 @@ url-loader@^2.0.1: mime "^2.4.4" schema-utils "^2.5.0" -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - url-parse@^1.4.3: version "1.4.7" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" @@ -15185,11 +14864,6 @@ url-parse@^1.4.3: querystringify "^2.1.1" requires-port "^1.0.0" -url-to-options@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" - integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= - url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -15793,7 +15467,7 @@ yargs-parser@^5.0.0: dependencies: camelcase "^3.0.0" -yargs@12.0.5, yargs@^12.0.2: +yargs@12.0.5: version "12.0.5" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==