From a1ff55d08432b11763b03315d29f77078c56b078 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 26 Nov 2019 13:22:00 +0100 Subject: [PATCH 01/57] override global radio stylings and use local styles instead --- scm-ui/ui-components/src/forms/Radio.tsx | 10 ++++++++-- scm-ui/ui-styles/src/scm.scss | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/scm-ui/ui-components/src/forms/Radio.tsx b/scm-ui/ui-components/src/forms/Radio.tsx index 429fc91077..e9aea098c4 100644 --- a/scm-ui/ui-components/src/forms/Radio.tsx +++ b/scm-ui/ui-components/src/forms/Radio.tsx @@ -1,5 +1,11 @@ import React, { ChangeEvent } from "react"; import { Help } from "../index"; +import styled from "styled-components"; + + +const StyledRadio = styled.label` + margin-right: 0.5em; +`; type Props = { label?: string; @@ -33,7 +39,7 @@ class Radio extends React.Component { because jsx label does not the custom disabled attribute but bulma does. // @ts-ignore */} - + ); } diff --git a/scm-ui/ui-styles/src/scm.scss b/scm-ui/ui-styles/src/scm.scss index 01d8d3e55a..82b6b609c3 100644 --- a/scm-ui/ui-styles/src/scm.scss +++ b/scm-ui/ui-styles/src/scm.scss @@ -809,6 +809,12 @@ form .field:not(.is-grouped) { } } +// radio +//overwrite bulma's default margin +.radio + .radio { + margin-left: 0; +} + // cursor .has-cursor-pointer { cursor: pointer; From 87904e3da81fa4917578d18f6a1812f1f1c465e1 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 10 Dec 2019 15:56:56 +0100 Subject: [PATCH 02/57] Stop fetching commits when this takes too long This is a first step to create results in big repositories. Next step should be querying the commit messages in the background and update cached results for further requests. --- .../scm/repository/spi/GitBrowseCommand.java | 89 +++++++++++-------- 1 file changed, 54 insertions(+), 35 deletions(-) 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..753802d3eb 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 @@ -69,6 +69,11 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static java.util.Optional.empty; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -94,6 +99,9 @@ public class GitBrowseCommand extends AbstractGitCommand LoggerFactory.getLogger(GitBrowseCommand.class); private final LfsBlobStoreFactory lfsBlobStoreFactory; + private boolean fetchCommits = true; + private ExecutorService executorService; + //~--- constructors --------------------------------------------------------- /** @@ -114,42 +122,36 @@ public class GitBrowseCommand extends AbstractGitCommand @SuppressWarnings("unchecked") public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { - logger.debug("try to create browse result for {}", request); + executorService = Executors.newSingleThreadExecutor(); + try { + logger.debug("try to create browse result for {}", request); - BrowserResult result; + org.eclipse.jgit.lib.Repository repo = open(); + ObjectId revId = computeRevIdToBrowse(request, repo); - org.eclipse.jgit.lib.Repository repo = open(); - ObjectId revId; - - if (Util.isEmpty(request.getRevision())) - { - revId = getDefaultBranch(repo); - } - else - { - revId = GitUtil.getRevisionId(repo, request.getRevision()); + if (revId != null) { + return new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); + } else { + logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); + return new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); + } + } finally { + executorService.shutdown(); } + } - 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 -------------------------------------------------------------- @@ -207,8 +209,19 @@ 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); + RevCommit commit = null; + if (fetchCommits) { + logger.trace("fetch last commit for {} at {}", path, revId.getName()); + Future f = executorService.submit(() -> getLatestCommit(repo, revId, path)); + try { + commit = f.get(100, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + logger.info("lookup of latest commit took too much time for {} at {}; disabling lookup on {}", path, revId.name(), repository.getNamespaceAndName()); + fetchCommits = false; + } catch (Exception e) { + logger.warn("got exception while computing last commit for {} on {}", path, revId.name(), e); + } + } Optional lfsPointer = commit == null? empty(): GitUtil.getLfsPointer(repo, path, commit, treeWalk); @@ -231,7 +244,7 @@ public class GitBrowseCommand extends AbstractGitCommand file.setLastModified(GitUtil.getCommitTime(commit)); file.setDescription(commit.getShortMessage()); } - else if (logger.isWarnEnabled()) + else if (fetchCommits) { logger.warn("could not find latest commit for {} on {}", path, revId); @@ -243,6 +256,8 @@ public class GitBrowseCommand extends AbstractGitCommand //~--- get methods ---------------------------------------------------------- +private RevWalk commitWalk = null; + /** * Method description * @@ -263,8 +278,12 @@ public class GitBrowseCommand extends AbstractGitCommand try { walk = new RevWalk(repo); - walk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path), - TreeFilter.ANY_DIFF)); + walk.setTreeFilter(AndTreeFilter.create( + TreeFilter.ANY_DIFF + , + PathFilter.create(path) + ) + ); RevCommit commit = walk.parseCommit(revId); @@ -404,8 +423,8 @@ public class GitBrowseCommand extends AbstractGitCommand } catch (NotFoundException ex) { - logger.trace("could not find .gitmodules", ex); - subRepositories = Collections.EMPTY_MAP; + logger.trace("could not find .gitmodules: {}", ex.getMessage()); + subRepositories = Collections.emptyMap(); } return subRepositories; From 6f4074c21cc628c2500bffc540c37591751fb86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 10 Dec 2019 18:10:11 +0100 Subject: [PATCH 03/57] Update browser result after computation --- .../repository/api/BrowseCommandBuilder.java | 11 ++- .../repository/spi/BrowseCommandRequest.java | 20 +++++ .../scm/repository/spi/GitBrowseCommand.java | 77 ++++++++++--------- .../repository/spi/GitBrowseCommandTest.java | 2 +- 4 files changed, 70 insertions(+), 40 deletions(-) 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..0c82c71331 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,6 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** browse file objects recursive */ private boolean recursive = false; + + private final Consumer updater; } 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 753802d3eb..6b02ab5fd7 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 @@ -56,6 +56,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; @@ -71,9 +72,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import static java.util.Optional.empty; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -99,7 +98,6 @@ public class GitBrowseCommand extends AbstractGitCommand LoggerFactory.getLogger(GitBrowseCommand.class); private final LfsBlobStoreFactory lfsBlobStoreFactory; - private boolean fetchCommits = true; private ExecutorService executorService; //~--- constructors --------------------------------------------------------- @@ -130,13 +128,25 @@ public class GitBrowseCommand extends AbstractGitCommand ObjectId revId = computeRevIdToBrowse(request, repo); if (revId != null) { - return new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); + BrowserResult browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); + executorService.execute(() -> { + request.updateCache(browserResult); + logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); + }); + return browserResult; } else { logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); return new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); } } finally { executorService.shutdown(); + try { + if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) { + logger.info("lookup of all commits took too long in repo {}", repository.getNamespaceAndName()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } } @@ -209,46 +219,39 @@ public class GitBrowseCommand extends AbstractGitCommand // don't show message and date for directories to improve performance if (!file.isDirectory() &&!request.isDisableLastCommit()) { - RevCommit commit = null; - if (fetchCommits) { + executorService.execute(() -> { logger.trace("fetch last commit for {} at {}", path, revId.getName()); - Future f = executorService.submit(() -> getLatestCommit(repo, revId, path)); + RevCommit commit = getLatestCommit(repo, revId, path); + + Optional lfsPointer; try { - commit = f.get(100, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - logger.info("lookup of latest commit took too much time for {} at {}; disabling lookup on {}", path, revId.name(), repository.getNamespaceAndName()); - fetchCommits = false; - } catch (Exception e) { - logger.warn("got exception while computing last commit for {} on {}", path, revId.name(), e); + lfsPointer = commit == null ? empty() : GitUtil.getLfsPointer(repo, path, commit, treeWalk); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "could not read lfs pointer", e); } - } - Optional lfsPointer = commit == null? empty(): GitUtil.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); + 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()); + } } else { - file.setLength(blob.getSize()); + file.setLength(loader.getSize()); } - } else { - file.setLength(loader.getSize()); - } - if (commit != null) - { - file.setLastModified(GitUtil.getCommitTime(commit)); - file.setDescription(commit.getShortMessage()); - } - else if (fetchCommits) - { - logger.warn("could not find latest commit for {} on {}", path, - revId); - } + if (commit != null) { + file.setLastModified(GitUtil.getCommitTime(commit)); + file.setDescription(commit.getShortMessage()); + } else { + logger.warn("could not find latest commit for {} on {}", path, + revId); + } + }); } } return file; 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..61d877bd4a 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 @@ -142,7 +142,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } @Test - public void testRecusive() throws IOException { + public void testRecursive() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setRecursive(true); From 8d0249b70882870cee76efbd89dcf6b2c6a1cbc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 10 Dec 2019 18:22:40 +0100 Subject: [PATCH 04/57] Make timeout configurable --- .../repository/api/BrowseCommandBuilder.java | 4 ++++ .../repository/spi/FileBaseCommandRequest.java | 17 ++++++++++++++++- .../scm/repository/spi/GitBrowseCommand.java | 6 ++---- 3 files changed, 22 insertions(+), 5 deletions(-) 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 563557f0c1..437e3f5fa0 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 @@ -300,6 +300,10 @@ public final class BrowseCommandBuilder return this; } + public void setComputationTimeoutMilliSeconds(long computationTimeoutMilliSeconds) { + request.setComputationTimeoutMilliSeconds(computationTimeoutMilliSeconds); + } + private void updateCache(BrowserResult updatedResult) { if (!disableCache) { CacheKey key = new CacheKey(repository, request); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index 9f563345fd..c6683905dd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -147,6 +147,10 @@ public abstract class FileBaseCommandRequest this.revision = revision; } + public void setComputationTimeoutMilliSeconds(long computationTimeoutMilliSeconds) { + this.computationTimeoutMilliSeconds = computationTimeoutMilliSeconds; + } + //~--- get methods ---------------------------------------------------------- /** @@ -171,7 +175,14 @@ public abstract class FileBaseCommandRequest return revision; } - //~--- methods -------------------------------------------------------------- + public boolean isDisableCommitValues() { + return disableCommitValues; + } + + public long getComputationTimeoutMilliSeconds() { + return computationTimeoutMilliSeconds; + } +//~--- methods -------------------------------------------------------------- /** * Method description @@ -208,4 +219,8 @@ public abstract class FileBaseCommandRequest /** Field description */ private String revision; + + private boolean disableCommitValues = false; + + private long computationTimeoutMilliSeconds = 1000; } 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 6b02ab5fd7..a82a592497 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 @@ -141,8 +141,8 @@ public class GitBrowseCommand extends AbstractGitCommand } finally { executorService.shutdown(); try { - if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) { - logger.info("lookup of all commits took too long in repo {}", repository.getNamespaceAndName()); + if (!executorService.awaitTermination(request.getComputationTimeoutMilliSeconds(), TimeUnit.MILLISECONDS)) { + logger.info("lookup of all commits aborted after {}ms in repo {}", request.getComputationTimeoutMilliSeconds(), repository.getNamespaceAndName()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -259,8 +259,6 @@ public class GitBrowseCommand extends AbstractGitCommand //~--- get methods ---------------------------------------------------------- -private RevWalk commitWalk = null; - /** * Method description * From ce15b116bd140f99d275766c7f562e40f714e0ef Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 10:10:56 +0100 Subject: [PATCH 05/57] Introduce SyncAsyncExecutor --- .../scm/repository/spi/SyncAsyncExecutor.java | 43 +++++++++++++++++ .../spi/SyncAsyncExecutorProvider.java | 29 +++++++++++ .../scm/repository/spi/GitBrowseCommand.java | 48 +++++++------------ .../spi/GitRepositoryServiceProvider.java | 7 ++- .../spi/GitRepositoryServiceResolver.java | 6 ++- .../repository/spi/GitBrowseCommandTest.java | 3 +- .../repository/spi/SyncAsyncExecutors.java | 10 ++++ 7 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java create mode 100644 scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java 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..86d37814da --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java @@ -0,0 +1,43 @@ +package sonia.scm.repository.spi; + +import java.time.Instant; +import java.util.concurrent.Executor; +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 SyncAsyncExecutor { + + private final Executor executor; + private final Instant switchToAsyncTime; + private boolean executedAllSynchronously = true; + + SyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) { + this.executor = executor; + this.switchToAsyncTime = switchToAsyncTime; + } + + public ExecutionType execute(Runnable runnable) { + return execute(ignored -> runnable.run()); + } + + public ExecutionType execute(Consumer runnable) { + if (Instant.now().isAfter(switchToAsyncTime)) { + executor.execute(() -> runnable.accept(ASYNCHRONOUS)); + executedAllSynchronously = false; + return ASYNCHRONOUS; + } else { + runnable.accept(SYNCHRONOUS); + return SYNCHRONOUS; + } + } + + public boolean hasExecutedAllSynchronously() { + return executedAllSynchronously; + } + + public 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..2c576f814f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java @@ -0,0 +1,29 @@ +package sonia.scm.repository.spi; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class SyncAsyncExecutorProvider { + + private static final int DEFAULT_TIMEOUT_SECONDS = 2; + + private final Executor executor; + + public SyncAsyncExecutorProvider() { + this(Executors.newFixedThreadPool(4)); + } + + public SyncAsyncExecutorProvider(Executor executor) { + this.executor = executor; + } + + public SyncAsyncExecutor createExecutorWithDefaultTimeout() { + return createExecutorWithSecondsToTimeout(DEFAULT_TIMEOUT_SECONDS); + } + + public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int seconds) { + return new SyncAsyncExecutor(executor, Instant.now().plus(seconds, ChronoUnit.SECONDS)); + } +} 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 a82a592497..670b43dd5c 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 @@ -70,9 +70,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import static java.util.Optional.empty; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -98,55 +95,46 @@ public class GitBrowseCommand extends AbstractGitCommand LoggerFactory.getLogger(GitBrowseCommand.class); private final LfsBlobStoreFactory lfsBlobStoreFactory; - private ExecutorService executorService; + private final SyncAsyncExecutor executor; //~--- constructors --------------------------------------------------------- /** * Constructs ... - * @param context + * @param context * @param repository * @param lfsBlobStoreFactory + * @param executor */ - public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) + 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 { - executorService = Executors.newSingleThreadExecutor(); - try { - logger.debug("try to create browse result for {}", request); + logger.debug("try to create browse result for {}", request); - org.eclipse.jgit.lib.Repository repo = open(); - ObjectId revId = computeRevIdToBrowse(request, repo); + org.eclipse.jgit.lib.Repository repo = open(); + ObjectId revId = computeRevIdToBrowse(request, repo); - if (revId != null) { - BrowserResult browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); - executorService.execute(() -> { + if (revId != null) { + BrowserResult browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); + executor.execute(executionType -> { + if (executionType == SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS) { request.updateCache(browserResult); logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); - }); - return browserResult; - } else { - logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); - return new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); - } - } finally { - executorService.shutdown(); - try { - if (!executorService.awaitTermination(request.getComputationTimeoutMilliSeconds(), TimeUnit.MILLISECONDS)) { - logger.info("lookup of all commits aborted after {}ms in repo {}", request.getComputationTimeoutMilliSeconds(), repository.getNamespaceAndName()); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + }); + return browserResult; + } else { + logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); + return new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); } } @@ -219,7 +207,7 @@ public class GitBrowseCommand extends AbstractGitCommand // don't show message and date for directories to improve performance if (!file.isDirectory() &&!request.isDisableLastCommit()) { - executorService.execute(() -> { + executor.execute(() -> { logger.trace("fetch last commit for {} at {}", path, revId.getName()); RevCommit commit = getLatestCommit(repo, revId, path); 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 61d877bd4a..40dfdf2159 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 @@ -44,6 +44,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static sonia.scm.repository.spi.SyncAsyncExecutors.synchronousExecutor; /** * Unit tests for {@link GitBrowseCommand}. @@ -171,6 +172,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-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..9f64a39474 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java @@ -0,0 +1,10 @@ +package sonia.scm.repository.spi; + +import java.time.Instant; + +public final class SyncAsyncExecutors { + + public static SyncAsyncExecutor synchronousExecutor() { + return new SyncAsyncExecutor(Runnable::run, Instant.MAX); + } +} From 9a8f0a4ee742a380dda186188d0107dcb6823c93 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 12:42:21 +0100 Subject: [PATCH 06/57] Use interfaces for executor classes --- .../scm/repository/spi/SyncAsyncExecutor.java | 37 +++-------------- .../spi/SyncAsyncExecutorProvider.java | 25 ++--------- .../modules/ApplicationModuleProvider.java | 2 + .../repository/DefaultSyncAsyncExecutor.java | 41 +++++++++++++++++++ .../DefaultSyncAsyncExecutorProvider.java | 32 +++++++++++++++ .../sonia/scm/repository/ExecutorModule.java | 12 ++++++ 6 files changed, 96 insertions(+), 53 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/ExecutorModule.java 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 index 86d37814da..8ba7af3155 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java @@ -1,43 +1,16 @@ package sonia.scm.repository.spi; -import java.time.Instant; -import java.util.concurrent.Executor; import java.util.function.Consumer; -import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS; -import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONOUS; +public interface SyncAsyncExecutor { -public class SyncAsyncExecutor { + ExecutionType execute(Runnable runnable); - private final Executor executor; - private final Instant switchToAsyncTime; - private boolean executedAllSynchronously = true; + ExecutionType execute(Consumer runnable); - SyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) { - this.executor = executor; - this.switchToAsyncTime = switchToAsyncTime; - } + boolean hasExecutedAllSynchronously(); - public ExecutionType execute(Runnable runnable) { - return execute(ignored -> runnable.run()); - } - - public ExecutionType execute(Consumer runnable) { - if (Instant.now().isAfter(switchToAsyncTime)) { - executor.execute(() -> runnable.accept(ASYNCHRONOUS)); - executedAllSynchronously = false; - return ASYNCHRONOUS; - } else { - runnable.accept(SYNCHRONOUS); - return SYNCHRONOUS; - } - } - - public boolean hasExecutedAllSynchronously() { - return executedAllSynchronously; - } - - public enum ExecutionType { + 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 index 2c576f814f..2d21129eb2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java @@ -1,29 +1,12 @@ package sonia.scm.repository.spi; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +public interface SyncAsyncExecutorProvider { -public class SyncAsyncExecutorProvider { + int DEFAULT_TIMEOUT_SECONDS = 2; - private static final int DEFAULT_TIMEOUT_SECONDS = 2; - - private final Executor executor; - - public SyncAsyncExecutorProvider() { - this(Executors.newFixedThreadPool(4)); - } - - public SyncAsyncExecutorProvider(Executor executor) { - this.executor = executor; - } - - public SyncAsyncExecutor createExecutorWithDefaultTimeout() { + default SyncAsyncExecutor createExecutorWithDefaultTimeout() { return createExecutorWithSecondsToTimeout(DEFAULT_TIMEOUT_SECONDS); } - public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int seconds) { - return new SyncAsyncExecutor(executor, Instant.now().plus(seconds, ChronoUnit.SECONDS)); - } + SyncAsyncExecutor createExecutorWithSecondsToTimeout(int seconds); } 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..232435aec5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java @@ -0,0 +1,41 @@ +package sonia.scm.repository; + +import sonia.scm.repository.spi.SyncAsyncExecutor; + +import java.time.Instant; +import java.util.concurrent.Executor; +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 boolean executedAllSynchronously = true; + + DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) { + this.executor = executor; + this.switchToAsyncTime = switchToAsyncTime; + } + + public ExecutionType execute(Runnable runnable) { + return execute(ignored -> runnable.run()); + } + + public ExecutionType execute(Consumer runnable) { + if (Instant.now().isAfter(switchToAsyncTime)) { + executor.execute(() -> runnable.accept(ASYNCHRONOUS)); + executedAllSynchronously = false; + return ASYNCHRONOUS; + } else { + runnable.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..0be7d38234 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java @@ -0,0 +1,32 @@ +package sonia.scm.repository; + +import sonia.scm.repository.spi.SyncAsyncExecutor; +import sonia.scm.repository.spi.SyncAsyncExecutorProvider; + +import java.io.Closeable; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class DefaultSyncAsyncExecutorProvider implements SyncAsyncExecutorProvider, Closeable { + + private final ExecutorService executor; + + public DefaultSyncAsyncExecutorProvider() { + this(Executors.newFixedThreadPool(4)); + } + + public DefaultSyncAsyncExecutorProvider(ExecutorService executor) { + this.executor = executor; + } + + public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int seconds) { + return new DefaultSyncAsyncExecutor(executor, Instant.now().plus(seconds, ChronoUnit.SECONDS)); + } + + @Override + public void close() { + executor.shutdownNow(); + } +} 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); + } +} From a37df2c20b396611f36fde20cd9738330422e51b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 12:58:01 +0100 Subject: [PATCH 07/57] Fix test helper --- .../scm/repository/spi/SyncAsyncExecutor.java | 4 +++- .../scm/repository/spi/SyncAsyncExecutors.java | 17 +++++++++++++++-- .../repository/DefaultSyncAsyncExecutor.java | 4 ---- 3 files changed, 18 insertions(+), 7 deletions(-) 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 index 8ba7af3155..55fdbcacdb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java @@ -4,7 +4,9 @@ import java.util.function.Consumer; public interface SyncAsyncExecutor { - ExecutionType execute(Runnable runnable); + default ExecutionType execute(Runnable runnable) { + return execute(ignored -> runnable.run()); + } ExecutionType execute(Consumer runnable); 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 index 9f64a39474..15a1bef046 100644 --- a/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java +++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java @@ -1,10 +1,23 @@ package sonia.scm.repository.spi; -import java.time.Instant; +import java.util.function.Consumer; + +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONOUS; public final class SyncAsyncExecutors { public static SyncAsyncExecutor synchronousExecutor() { - return new SyncAsyncExecutor(Runnable::run, Instant.MAX); + return new SyncAsyncExecutor() { + @Override + public ExecutionType execute(Consumer runnable) { + runnable.accept(SYNCHRONOUS); + return SYNCHRONOUS; + } + + @Override + public boolean hasExecutedAllSynchronously() { + return true; + } + }; } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java index 232435aec5..326b329218 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java @@ -20,10 +20,6 @@ public class DefaultSyncAsyncExecutor implements SyncAsyncExecutor { this.switchToAsyncTime = switchToAsyncTime; } - public ExecutionType execute(Runnable runnable) { - return execute(ignored -> runnable.run()); - } - public ExecutionType execute(Consumer runnable) { if (Instant.now().isAfter(switchToAsyncTime)) { executor.execute(() -> runnable.accept(ASYNCHRONOUS)); From 7c0eb9251ae60c1e681c5b6197f89fa11598a10a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 13:00:07 +0100 Subject: [PATCH 08/57] Add unit test --- .../DefaultSyncAsyncExecutorTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java 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..bd4148606b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java @@ -0,0 +1,38 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType; + +import java.time.Instant; + +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; + + @Test + void shouldExecuteSynchronouslyBeforeTimeout() { + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.MAX); + + ExecutionType result = executor.execute(type -> calledWithType = type); + + assertThat(result).isEqualTo(SYNCHRONOUS); + assertThat(calledWithType).isEqualTo(SYNCHRONOUS); + assertThat(executor.hasExecutedAllSynchronously()).isTrue(); + } + + @Test + void shouldExecuteAsynchronouslyAfterTimeout() { + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS)); + + ExecutionType result = executor.execute(type -> calledWithType = type); + + assertThat(result).isEqualTo(ASYNCHRONOUS); + assertThat(calledWithType).isEqualTo(ASYNCHRONOUS); + assertThat(executor.hasExecutedAllSynchronously()).isFalse(); + } +} From 093c0abb0213e08fd7de30105e8acbc6feafd81a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 13:32:05 +0100 Subject: [PATCH 09/57] Add unit test for asynchronous browse --- .../repository/spi/GitBrowseCommandTest.java | 25 +++++++ .../repository/spi/SyncAsyncExecutors.java | 71 +++++++++++++++++++ 2 files changed, 96 insertions(+) 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 40dfdf2159..0e36c39499 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,6 +35,7 @@ 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; @@ -43,7 +44,9 @@ 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; /** @@ -112,6 +115,28 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertEquals("c", c.getPath()); } + @Test + public void testAsynchronousBrowse() throws IOException { + try (AsyncExecutorStepper executor = stepperAsynchronousExecutor()) { + GitBrowseCommand command = new GitBrowseCommand(createContext(), repository, null, executor); + FileObject root = command.getBrowserResult(new BrowseCommandRequest()).getFile(); + assertNotNull(root); + + Collection foList = root.getChildren(); + + FileObject a = findFile(foList, "a.txt"); + + assertFalse(a.isDirectory()); + assertNull("expected empty name before commit could have been read", a.getDescription()); + assertNull("expected empty date before commit could have been read", a.getLastModified()); + + executor.next(); + + assertNotNull("expected correct name after commit could have been read", a.getDescription()); + assertNotNull("expected correct date after commit could have been read", a.getLastModified()); + } + } + @Test public void testBrowseSubDirectory() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); 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 index 15a1bef046..9dbe26403b 100644 --- a/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java +++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java @@ -1,7 +1,12 @@ 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 { @@ -20,4 +25,70 @@ public final class SyncAsyncExecutors { } }; } + + public static SyncAsyncExecutor asynchronousExecutor() { + + Executor executor = Executors.newSingleThreadExecutor(); + + return new SyncAsyncExecutor() { + @Override + public ExecutionType execute(Consumer runnable) { + executor.execute(() -> runnable.accept(ASYNCHRONOUS)); + return ASYNCHRONOUS; + } + + @Override + public boolean hasExecutedAllSynchronously() { + return true; + } + }; + } + + public static AsyncExecutorStepper stepperAsynchronousExecutor() { + + Executor executor = Executors.newSingleThreadExecutor(); + Semaphore enterSemaphore = new Semaphore(0); + Semaphore exitSemaphore = new Semaphore(0); + + return new AsyncExecutorStepper() { + @Override + public void close() { + enterSemaphore.release(Integer.MAX_VALUE); + exitSemaphore.release(Integer.MAX_VALUE); + } + + @Override + public ExecutionType execute(Consumer runnable) { + executor.execute(() -> { + try { + enterSemaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + runnable.accept(ASYNCHRONOUS); + exitSemaphore.release(); + }); + return ASYNCHRONOUS; + } + + @Override + public void next() { + enterSemaphore.release(); + try { + exitSemaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public boolean hasExecutedAllSynchronously() { + return true; + } + }; + } + + public interface AsyncExecutorStepper extends SyncAsyncExecutor, Closeable { + void next(); + } } From 9caf6c09846f07146adfc00378d9fc49ccddb3ed Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 14:01:17 +0100 Subject: [PATCH 10/57] Update cache after each file --- .../scm/repository/spi/GitBrowseCommand.java | 13 ++++++++++--- .../repository/spi/GitBrowseCommandTest.java | 19 +++++++++++++++++-- .../repository/spi/SyncAsyncExecutors.java | 4 ++-- 3 files changed, 29 insertions(+), 7 deletions(-) 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 670b43dd5c..bdeb425d15 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 @@ -74,6 +74,7 @@ import java.util.Optional; import static java.util.Optional.empty; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS; //~--- JDK imports ------------------------------------------------------------ @@ -97,6 +98,8 @@ public class GitBrowseCommand extends AbstractGitCommand private final SyncAsyncExecutor executor; + private BrowserResult browserResult; + //~--- constructors --------------------------------------------------------- /** @@ -124,9 +127,9 @@ public class GitBrowseCommand extends AbstractGitCommand ObjectId revId = computeRevIdToBrowse(request, repo); if (revId != null) { - BrowserResult browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); + browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); executor.execute(executionType -> { - if (executionType == SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS) { + if (executionType == ASYNCHRONOUS) { request.updateCache(browserResult); logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); } @@ -207,7 +210,7 @@ public class GitBrowseCommand extends AbstractGitCommand // don't show message and date for directories to improve performance if (!file.isDirectory() &&!request.isDisableLastCommit()) { - executor.execute(() -> { + executor.execute(executionType -> { logger.trace("fetch last commit for {} at {}", path, revId.getName()); RevCommit commit = getLatestCommit(repo, revId, path); @@ -235,6 +238,10 @@ public class GitBrowseCommand extends AbstractGitCommand if (commit != null) { file.setLastModified(GitUtil.getCommitTime(commit)); file.setDescription(commit.getShortMessage()); + if (executionType == ASYNCHRONOUS && browserResult != null) { + request.updateCache(browserResult); + logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); + } } else { logger.warn("could not find latest commit for {} on {}", path, revId); 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 0e36c39499..03f1e761c7 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 @@ -39,6 +39,8 @@ 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; @@ -119,21 +121,34 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { public void testAsynchronousBrowse() throws IOException { try (AsyncExecutorStepper executor = stepperAsynchronousExecutor()) { GitBrowseCommand command = new GitBrowseCommand(createContext(), repository, null, executor); - FileObject root = command.getBrowserResult(new BrowseCommandRequest()).getFile(); + 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"); - assertFalse(a.isDirectory()); assertNull("expected empty name before commit could have been read", a.getDescription()); assertNull("expected empty date before commit could have been read", a.getLastModified()); + assertNull("expected empty name before commit could have been read", b.getDescription()); + assertNull("expected empty date before commit could have been read", b.getLastModified()); executor.next(); + assertEquals(1, updatedResults.size()); assertNotNull("expected correct name after commit could have been read", a.getDescription()); assertNotNull("expected correct date after commit could have been read", a.getLastModified()); + assertNull("expected empty name before commit could have been read", b.getDescription()); + assertNull("expected empty date before commit could have been read", b.getLastModified()); + + executor.next(); + + assertEquals(2, updatedResults.size()); + assertNotNull("expected correct name after commit could have been read", b.getDescription()); + assertNotNull("expected correct date after commit could have been read", b.getLastModified()); } } 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 index 9dbe26403b..aa7e7bb954 100644 --- a/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java +++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java @@ -53,8 +53,8 @@ public final class SyncAsyncExecutors { return new AsyncExecutorStepper() { @Override public void close() { - enterSemaphore.release(Integer.MAX_VALUE); - exitSemaphore.release(Integer.MAX_VALUE); + enterSemaphore.release(Integer.MAX_VALUE/2); + exitSemaphore.release(Integer.MAX_VALUE/2); } @Override From 8129f2fad05746305c0b7f2806dffb5deb987a9d Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 14:41:42 +0100 Subject: [PATCH 11/57] Add 'partial' flag to files --- .../src/main/java/sonia/scm/repository/FileObject.java | 10 ++++++++++ .../sonia/scm/repository/spi/GitBrowseCommand.java | 2 ++ .../sonia/scm/repository/spi/GitBrowseCommandTest.java | 5 +++++ 3 files changed, 17 insertions(+) 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..2f3241bf40 100644 --- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java +++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java @@ -222,6 +222,10 @@ public class FileObject implements LastModifiedAware, Serializable return directory; } + public boolean isPartialResult() { + return partialResult; + } + //~--- set methods ---------------------------------------------------------- /** @@ -302,6 +306,10 @@ public class FileObject implements LastModifiedAware, Serializable this.subRepository = subRepository; } + public void setPartialResult(boolean partialResult) { + this.partialResult = partialResult; + } + public Collection getChildren() { return unmodifiableCollection(children); } @@ -338,6 +346,8 @@ public class FileObject implements LastModifiedAware, Serializable /** file path */ private String path; + private boolean partialResult = false; + /** sub repository informations */ @XmlElement(name = "subrepository") private SubRepository subRepository; 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 bdeb425d15..06b724d820 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 @@ -210,6 +210,7 @@ public class GitBrowseCommand extends AbstractGitCommand // don't show message and date for directories to improve performance if (!file.isDirectory() &&!request.isDisableLastCommit()) { + file.setPartialResult(true); executor.execute(executionType -> { logger.trace("fetch last commit for {} at {}", path, revId.getName()); RevCommit commit = getLatestCommit(repo, revId, path); @@ -235,6 +236,7 @@ public class GitBrowseCommand extends AbstractGitCommand file.setLength(loader.getSize()); } + file.setPartialResult(false); if (commit != null) { file.setLastModified(GitUtil.getCommitTime(commit)); file.setDescription(commit.getShortMessage()); 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 03f1e761c7..9b45cda16f 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 @@ -131,22 +131,27 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { FileObject a = findFile(foList, "a.txt"); FileObject b = findFile(foList, "b.txt"); + assertTrue(a.isPartialResult()); assertNull("expected empty name before commit could have been read", a.getDescription()); assertNull("expected empty date before commit could have been read", a.getLastModified()); + assertTrue(b.isPartialResult()); assertNull("expected empty name before commit could have been read", b.getDescription()); assertNull("expected empty date before commit could have been read", b.getLastModified()); executor.next(); assertEquals(1, updatedResults.size()); + assertFalse(a.isPartialResult()); assertNotNull("expected correct name after commit could have been read", a.getDescription()); assertNotNull("expected correct date after commit could have been read", a.getLastModified()); + assertTrue(b.isPartialResult()); assertNull("expected empty name before commit could have been read", b.getDescription()); assertNull("expected empty date before commit could have been read", b.getLastModified()); executor.next(); assertEquals(2, updatedResults.size()); + assertFalse(b.isPartialResult()); assertNotNull("expected correct name after commit could have been read", b.getDescription()); assertNotNull("expected correct date after commit could have been read", b.getLastModified()); } From 58cff0797b215c8f236e819a518a068fa45dea9b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 15:09:53 +0100 Subject: [PATCH 12/57] Remove redundant mapper FileObjectToFileObjectDtoMapper#map and BrowserResultToFileObjectDtoMapper#fileObjectToDto had the same mapstruct implementation. --- .../BrowserResultToFileObjectDtoMapper.java | 15 +-- .../FileObjectToFileObjectDtoMapper.java | 22 ---- .../scm/api/v2/resources/MapperModule.java | 1 - ...rowserResultToFileObjectDtoMapperTest.java | 5 - .../FileObjectToFileObjectDtoMapperTest.java | 121 ------------------ .../v2/resources/SourceRootResourceTest.java | 7 +- 6 files changed, 2 insertions(+), 169 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java delete mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java 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..866c2cc0b9 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; @@ -20,14 +19,6 @@ import java.lang.annotation.Target; @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 +27,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) { 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/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java index 162a90d45a..27ef82834b 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); @@ -40,7 +36,6 @@ public class BrowserResultToFileObjectDtoMapperTest { public void init() { initMocks(this); mapper = Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class); - mapper.setChildrenMapper(fileObjectToFileObjectDtoMapper); mapper.setResourceLinks(resourceLinks); subjectThreadState.bind(); 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..7b205c732a 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); From f7dc89ee81d1e8c51cefc443c2e5ed6beb8abb1a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Dec 2019 15:49:33 +0100 Subject: [PATCH 13/57] Reload partial results --- scm-ui/ui-types/src/Sources.ts | 1 + .../src/repos/sources/modules/sources.ts | 18 ++++++++++++++++-- .../scm/api/v2/resources/FileObjectDto.java | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index 4e0e7fe188..7b80b9df94 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -16,6 +16,7 @@ export type File = { length: number; lastModified?: string; subRepository?: SubRepository; // TODO + partialResult: boolean; _links: Links; _embedded: { children: File[] | null | undefined; 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..b989e18b34 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -10,13 +10,27 @@ 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 fetchSourcesWithoutOptionalLoadingState(repository, revision, path, true); +} + +export function fetchSourcesWithoutOptionalLoadingState( + repository: Repository, + revision: string, + path: string, + dispatchLoading: boolean +) { return function(dispatch: any) { - dispatch(fetchSourcesPending(repository, revision, path)); + if (dispatchLoading) { + dispatch(fetchSourcesPending(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)); + if (sources._embedded.children && sources._embedded.children.find(c => c.partialResult)) { + setTimeout(() => dispatch(fetchSourcesWithoutOptionalLoadingState(repository, revision, path, false)), 1000); + } }) .catch(err => { dispatch(fetchSourcesFailure(repository, revision, path, err)); 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..626af4bac5 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 @@ -27,6 +27,7 @@ public class FileObjectDto extends HalRepresentation { private SubRepositoryDto subRepository; @JsonInclude(JsonInclude.Include.NON_EMPTY) private String revision; + private boolean partialResult; public FileObjectDto(Links links, Embedded embedded) { super(links, embedded); From 8df43e7b4e352851972a34d19b124fce181f9830 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 12 Dec 2019 11:47:03 +0100 Subject: [PATCH 14/57] Let background computations abort for browse command --- .../java/sonia/scm/repository/FileObject.java | 10 +++++ .../scm/repository/spi/SyncAsyncExecutor.java | 8 +++- .../scm/repository/spi/GitBrowseCommand.java | 7 +++- .../repository/spi/SyncAsyncExecutors.java | 33 ++++++++++++----- scm-ui/ui-types/src/Sources.ts | 1 + scm-ui/ui-webapp/public/locales/de/repos.json | 4 +- scm-ui/ui-webapp/public/locales/en/repos.json | 4 +- .../repos/sources/components/FileTreeLeaf.tsx | 37 +++++++++++++++---- .../src/repos/sources/modules/sources.ts | 2 +- .../scm/api/v2/resources/FileObjectDto.java | 1 + .../repository/DefaultSyncAsyncExecutor.java | 26 +++++++++++-- .../DefaultSyncAsyncExecutorTest.java | 18 ++++++++- 12 files changed, 122 insertions(+), 29 deletions(-) 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 2f3241bf40..acb7559094 100644 --- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java +++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java @@ -226,6 +226,10 @@ public class FileObject implements LastModifiedAware, Serializable return partialResult; } + public boolean isComputationAborted() { + return computationAborted; + } + //~--- set methods ---------------------------------------------------------- /** @@ -310,6 +314,10 @@ public class FileObject implements LastModifiedAware, Serializable this.partialResult = partialResult; } + public void setComputationAborted(boolean computationAborted) { + this.computationAborted = computationAborted; + } + public Collection getChildren() { return unmodifiableCollection(children); } @@ -348,6 +356,8 @@ public class FileObject implements LastModifiedAware, Serializable private boolean partialResult = false; + private boolean computationAborted = false; + /** sub repository informations */ @XmlElement(name = "subrepository") private SubRepository subRepository; 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 index 55fdbcacdb..0cd3200736 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java @@ -5,10 +5,14 @@ import java.util.function.Consumer; public interface SyncAsyncExecutor { default ExecutionType execute(Runnable runnable) { - return execute(ignored -> runnable.run()); + return execute(ignored -> runnable.run(), () -> {}); } - ExecutionType execute(Consumer runnable); + default ExecutionType execute(Runnable runnable, Runnable abortionFallback) { + return execute(ignored -> runnable.run(), abortionFallback); + } + + ExecutionType execute(Consumer runnable, Runnable abortionFallback); boolean hasExecutedAllSynchronously(); 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 06b724d820..de479a4250 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 @@ -133,7 +133,7 @@ public class GitBrowseCommand extends AbstractGitCommand request.updateCache(browserResult); logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); } - }); + }, () -> {}); return browserResult; } else { logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); @@ -248,6 +248,11 @@ public class GitBrowseCommand extends AbstractGitCommand logger.warn("could not find latest commit for {} on {}", path, revId); } + }, () -> { + file.setPartialResult(false); + file.setComputationAborted(true); + request.updateCache(browserResult); + logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); }); } } 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 index aa7e7bb954..6c24e72b69 100644 --- a/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java +++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java @@ -14,7 +14,7 @@ public final class SyncAsyncExecutors { public static SyncAsyncExecutor synchronousExecutor() { return new SyncAsyncExecutor() { @Override - public ExecutionType execute(Consumer runnable) { + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { runnable.accept(SYNCHRONOUS); return SYNCHRONOUS; } @@ -32,7 +32,7 @@ public final class SyncAsyncExecutors { return new SyncAsyncExecutor() { @Override - public ExecutionType execute(Consumer runnable) { + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { executor.execute(() -> runnable.accept(ASYNCHRONOUS)); return ASYNCHRONOUS; } @@ -45,12 +45,13 @@ public final class SyncAsyncExecutors { } public static AsyncExecutorStepper stepperAsynchronousExecutor() { - - Executor executor = Executors.newSingleThreadExecutor(); - Semaphore enterSemaphore = new Semaphore(0); - Semaphore exitSemaphore = new Semaphore(0); - 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); @@ -58,15 +59,19 @@ public final class SyncAsyncExecutors { } @Override - public ExecutionType execute(Consumer runnable) { + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { executor.execute(() -> { try { enterSemaphore.acquire(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - runnable.accept(ASYNCHRONOUS); - exitSemaphore.release(); + if (timedOut) { + abortionFallback.run(); + } else { + runnable.accept(ASYNCHRONOUS); + exitSemaphore.release(); + } }); return ASYNCHRONOUS; } @@ -81,6 +86,12 @@ public final class SyncAsyncExecutors { } } + @Override + public void timeout() { + timedOut = true; + close(); + } + @Override public boolean hasExecutedAllSynchronously() { return true; @@ -90,5 +101,7 @@ public final class SyncAsyncExecutors { public interface AsyncExecutorStepper extends SyncAsyncExecutor, Closeable { void next(); + + void timeout(); } } diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index 7b80b9df94..f5fca71d65 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -17,6 +17,7 @@ export type File = { lastModified?: string; subRepository?: SubRepository; // TODO partialResult: boolean; + computationAborted: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index df6c534ece..5a357efd95 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 103e30b825..c1e7a835cd 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/FileTreeLeaf.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx index 961d65c043..037d8f7c45 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; }; @@ -31,7 +33,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); }; @@ -58,6 +60,25 @@ export default class FileTreeLeaf extends React.Component { return {file.name}; }; + contentIfPresent = (file: File, content: any) => { + const { t } = this.props; + if (file.computationAborted) { + return ( + + + + ); + } else if (file.partialResult) { + return ( + + + + ); + } else { + return content; + } + }; + render() { const { file } = this.props; @@ -68,10 +89,10 @@ export default class FileTreeLeaf extends React.Component { {this.createFileIcon(file)} {this.createFileName(file)} {fileSize} - - - - {file.description} + {this.contentIfPresent(file, )} + + {this.contentIfPresent(file, file.description)} + {binder.hasExtension("repos.sources.tree.row.right") && ( {!file.directory && ( @@ -89,3 +110,5 @@ export default class FileTreeLeaf extends React.Component { ); } } + +export default withTranslation("repos")(FileTreeLeaf); 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 b989e18b34..7d2b8c94a7 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -29,7 +29,7 @@ export function fetchSourcesWithoutOptionalLoadingState( .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, sources)); if (sources._embedded.children && sources._embedded.children.find(c => c.partialResult)) { - setTimeout(() => dispatch(fetchSourcesWithoutOptionalLoadingState(repository, revision, path, false)), 1000); + setTimeout(() => dispatch(fetchSourcesWithoutOptionalLoadingState(repository, revision, path, false)), 3000); } }) .catch(err => { 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 626af4bac5..4676a0fb03 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 @@ -28,6 +28,7 @@ public class FileObjectDto extends HalRepresentation { @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/repository/DefaultSyncAsyncExecutor.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java index 326b329218..6bdfa2d39f 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java @@ -4,6 +4,7 @@ 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; @@ -11,18 +12,35 @@ import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONO public class DefaultSyncAsyncExecutor implements SyncAsyncExecutor { + public static final long DEFAULT_MAX_ASYNC_RUNTIME = 60 * 1000L; + private final Executor executor; private final Instant switchToAsyncTime; + private final long maxAsyncRuntime; + private AtomicLong asyncRuntime = new AtomicLong(0L); private boolean executedAllSynchronously = true; DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) { - this.executor = executor; - this.switchToAsyncTime = switchToAsyncTime; + this(executor, switchToAsyncTime, DEFAULT_MAX_ASYNC_RUNTIME); } - public ExecutionType execute(Consumer runnable) { + DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime, long maxAsyncRuntime) { + this.executor = executor; + this.switchToAsyncTime = switchToAsyncTime; + this.maxAsyncRuntime = maxAsyncRuntime; + } + + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { if (Instant.now().isAfter(switchToAsyncTime)) { - executor.execute(() -> runnable.accept(ASYNCHRONOUS)); + executor.execute(() -> { + if (asyncRuntime.get() < maxAsyncRuntime) { + long chunkStartTime = System.currentTimeMillis(); + runnable.accept(ASYNCHRONOUS); + asyncRuntime.addAndGet(System.currentTimeMillis() - chunkStartTime); + } else { + abortionFallback.run(); + } + }); executedAllSynchronously = false; return ASYNCHRONOUS; } else { diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java index bd4148606b..6baca6204e 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java @@ -13,26 +13,40 @@ import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONO class DefaultSyncAsyncExecutorTest { ExecutionType calledWithType = null; + boolean aborted = false; @Test void shouldExecuteSynchronouslyBeforeTimeout() { DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.MAX); - ExecutionType result = executor.execute(type -> calledWithType = type); + 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)); - ExecutionType result = executor.execute(type -> calledWithType = type); + 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), 0L); + + ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); + + assertThat(result).isEqualTo(ASYNCHRONOUS); + assertThat(calledWithType).isNull(); + assertThat(aborted).isTrue(); } } From 6c8820719ecbe681bbc32519e683e1fb8057c966 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 12 Dec 2019 11:52:44 +0100 Subject: [PATCH 15/57] File size is unknown when not computed --- scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 037d8f7c45..9190a02dd6 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx @@ -88,7 +88,7 @@ class FileTreeLeaf extends React.Component { {this.createFileIcon(file)} {this.createFileName(file)} - {fileSize} + {this.contentIfPresent(file, fileSize)} {this.contentIfPresent(file, )} {this.contentIfPresent(file, file.description)} From 4fd2a0dd23888126a32830450fd8821d7a65e956 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 12 Dec 2019 16:13:36 +0100 Subject: [PATCH 16/57] Small API changes --- .../scm/repository/spi/SyncAsyncExecutor.java | 71 +++++++++++++++++-- .../spi/SyncAsyncExecutorProvider.java | 50 ++++++++++++- .../repository/DefaultSyncAsyncExecutor.java | 26 +++---- .../DefaultSyncAsyncExecutorProvider.java | 25 ++++++- .../DefaultSyncAsyncExecutorTest.java | 8 ++- 5 files changed, 150 insertions(+), 30 deletions(-) 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 index 0cd3200736..a9e7d85dcc 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java @@ -2,18 +2,79 @@ 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 { - default ExecutionType execute(Runnable runnable) { - return execute(ignored -> runnable.run(), () -> {}); + /** + * 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(), + () -> {} + ); } - default ExecutionType execute(Runnable runnable, Runnable abortionFallback) { - return execute(ignored -> runnable.run(), abortionFallback); + /** + * 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); } - ExecutionType execute(Consumer runnable, Runnable 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 { 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 index 2d21129eb2..5f417f324e 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java @@ -1,12 +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_TIMEOUT_SECONDS = 2; + 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_TIMEOUT_SECONDS); + return createExecutorWithSecondsToTimeout(DEFAULT_SWITCH_TO_ASYNC_IN_SECONDS); } - SyncAsyncExecutor createExecutorWithSecondsToTimeout(int 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-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java index 6bdfa2d39f..de9e27ecda 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java @@ -12,31 +12,25 @@ import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONO public class DefaultSyncAsyncExecutor implements SyncAsyncExecutor { - public static final long DEFAULT_MAX_ASYNC_RUNTIME = 60 * 1000L; - private final Executor executor; private final Instant switchToAsyncTime; - private final long maxAsyncRuntime; - private AtomicLong asyncRuntime = new AtomicLong(0L); + private final long maxAsyncAbortMilliseconds; + private AtomicLong accumulatedAsyncRuntime = new AtomicLong(0L); private boolean executedAllSynchronously = true; - DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) { - this(executor, switchToAsyncTime, DEFAULT_MAX_ASYNC_RUNTIME); - } - - DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime, long maxAsyncRuntime) { + DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime, int maxAsyncAbortSeconds) { this.executor = executor; this.switchToAsyncTime = switchToAsyncTime; - this.maxAsyncRuntime = maxAsyncRuntime; + this.maxAsyncAbortMilliseconds = maxAsyncAbortSeconds * 1000L; } - public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { - if (Instant.now().isAfter(switchToAsyncTime)) { + public ExecutionType execute(Consumer task, Runnable abortionFallback) { + if (switchToAsyncTime.isBefore(Instant.now())) { executor.execute(() -> { - if (asyncRuntime.get() < maxAsyncRuntime) { + if (accumulatedAsyncRuntime.get() < maxAsyncAbortMilliseconds) { long chunkStartTime = System.currentTimeMillis(); - runnable.accept(ASYNCHRONOUS); - asyncRuntime.addAndGet(System.currentTimeMillis() - chunkStartTime); + task.accept(ASYNCHRONOUS); + accumulatedAsyncRuntime.addAndGet(System.currentTimeMillis() - chunkStartTime); } else { abortionFallback.run(); } @@ -44,7 +38,7 @@ public class DefaultSyncAsyncExecutor implements SyncAsyncExecutor { executedAllSynchronously = false; return ASYNCHRONOUS; } else { - runnable.accept(SYNCHRONOUS); + task.accept(SYNCHRONOUS); return SYNCHRONOUS; } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java index 0be7d38234..4c1f0429da 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java @@ -11,22 +11,41 @@ import java.util.concurrent.Executors; 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; public DefaultSyncAsyncExecutorProvider() { - this(Executors.newFixedThreadPool(4)); + 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 seconds) { - return new DefaultSyncAsyncExecutor(executor, Instant.now().plus(seconds, ChronoUnit.SECONDS)); + public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds) { + return createExecutorWithSecondsToTimeout(switchToAsyncInSeconds, DEFAULT_MAX_ASYNC_ABORT_SECONDS); + } + + 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/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java index 6baca6204e..7e4ad60157 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java @@ -5,6 +5,8 @@ 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; @@ -17,7 +19,7 @@ class DefaultSyncAsyncExecutorTest { @Test void shouldExecuteSynchronouslyBeforeTimeout() { - DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.MAX); + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, MAX, MAX_VALUE); ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); @@ -29,7 +31,7 @@ class DefaultSyncAsyncExecutorTest { @Test void shouldExecuteAsynchronouslyAfterTimeout() { - DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS)); + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS), MAX_VALUE); ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); @@ -41,7 +43,7 @@ class DefaultSyncAsyncExecutorTest { @Test void shouldCallFallbackAfterAbortion() { - DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS), 0L); + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS), 0); ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); From 211aa15399cd03596a50a35b2b029df305481b69 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 12 Dec 2019 16:29:42 +0100 Subject: [PATCH 17/57] Make tasks explicit --- .../scm/repository/spi/GitBrowseCommand.java | 138 ++++++++++++------ 1 file changed, 95 insertions(+), 43 deletions(-) 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 de479a4250..18b8b628f1 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 @@ -70,6 +70,7 @@ 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 sonia.scm.ContextEntry.ContextBuilder.entity; @@ -211,49 +212,10 @@ public class GitBrowseCommand extends AbstractGitCommand if (!file.isDirectory() &&!request.isDisableLastCommit()) { file.setPartialResult(true); - executor.execute(executionType -> { - logger.trace("fetch last commit for {} at {}", path, revId.getName()); - RevCommit commit = getLatestCommit(repo, revId, path); - - Optional lfsPointer; - try { - lfsPointer = commit == null ? empty() : GitUtil.getLfsPointer(repo, path, commit, treeWalk); - } catch (IOException e) { - throw new InternalRepositoryException(repository, "could not read lfs pointer", e); - } - - 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()); - } - } else { - file.setLength(loader.getSize()); - } - - file.setPartialResult(false); - if (commit != null) { - file.setLastModified(GitUtil.getCommitTime(commit)); - file.setDescription(commit.getShortMessage()); - if (executionType == ASYNCHRONOUS && browserResult != null) { - request.updateCache(browserResult); - logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); - } - } else { - logger.warn("could not find latest commit for {} on {}", path, - revId); - } - }, () -> { - file.setPartialResult(false); - file.setComputationAborted(true); - request.updateCache(browserResult); - logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); - }); + executor.execute( + new CompleteFileInformation(path, revId, repo, treeWalk, file, loader, request), + new AbortFileInformation(file, request) + ); } } return file; @@ -458,4 +420,94 @@ public class GitBrowseCommand extends AbstractGitCommand /** sub repository cache */ private final Map> subrepositoryCache = Maps.newHashMap(); + + private class CompleteFileInformation implements Consumer { + private final String path; + private final ObjectId revId; + private final org.eclipse.jgit.lib.Repository repo; + private final TreeWalk treeWalk; + private final FileObject file; + private final ObjectLoader loader; + private final BrowseCommandRequest request; + + public CompleteFileInformation(String path, ObjectId revId, org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, FileObject file, ObjectLoader loader, BrowseCommandRequest request) { + this.path = path; + this.revId = revId; + this.repo = repo; + this.treeWalk = treeWalk; + this.file = file; + this.loader = loader; + this.request = request; + } + + @Override + public void accept(SyncAsyncExecutor.ExecutionType executionType) { + logger.trace("fetch last commit for {} at {}", path, revId.getName()); + RevCommit commit = GitBrowseCommand.this.getLatestCommit(repo, revId, path); + + Optional lfsPointer = getLfsPointer(commit); + + if (lfsPointer.isPresent()) { + setFileLengthFromLfsBlob(lfsPointer.get()); + } else { + file.setLength(loader.getSize()); + } + + file.setPartialResult(false); + if (commit != null) { + applyValuesFromCommit(executionType, commit); + } else { + logger.warn("could not find latest commit for {} on {}", path, revId); + } + } + + private void setFileLengthFromLfsBlob(LfsPointer lfsPointer) { + 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(-1); + } else { + file.setLength(blob.getSize()); + } + } + + private void applyValuesFromCommit(SyncAsyncExecutor.ExecutionType executionType, RevCommit commit) { + file.setLastModified(GitUtil.getCommitTime(commit)); + file.setDescription(commit.getShortMessage()); + if (executionType == ASYNCHRONOUS && browserResult != null) { + request.updateCache(browserResult); + logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); + } + } + + private Optional getLfsPointer(RevCommit commit) { + Optional lfsPointer; + try { + lfsPointer = commit == null ? empty() : GitUtil.getLfsPointer(repo, path, commit, treeWalk); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "could not read lfs pointer", e); + } + return lfsPointer; + } + } + + private class AbortFileInformation implements Runnable { + private final FileObject file; + private final BrowseCommandRequest request; + + public AbortFileInformation(FileObject file, BrowseCommandRequest request) { + this.file = file; + this.request = request; + } + + @Override + public void run() { + file.setPartialResult(false); + file.setComputationAborted(true); + request.updateCache(browserResult); + logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); + } + } } From 1beaf9d53a97c7e0f562c5a1082db98982898a4c Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 12 Dec 2019 17:03:37 +0100 Subject: [PATCH 18/57] Cleanup --- .../scm/repository/spi/GitBrowseCommand.java | 142 +++++------------- 1 file changed, 35 insertions(+), 107 deletions(-) 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 18b8b628f1..7927d38b2c 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 @@ -73,6 +73,7 @@ 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; @@ -101,24 +102,12 @@ public class GitBrowseCommand extends AbstractGitCommand private BrowserResult browserResult; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * @param context - * @param repository - * @param lfsBlobStoreFactory - * @param executor - */ - public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) - { + public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) { super(context, repository); this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.executor = executor; } - //~--- get methods ---------------------------------------------------------- - @Override public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { @@ -131,8 +120,7 @@ public class GitBrowseCommand extends AbstractGitCommand browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); executor.execute(executionType -> { if (executionType == ASYNCHRONOUS) { - request.updateCache(browserResult); - logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); + updateCache(request); } }, () -> {}); return browserResult; @@ -156,8 +144,6 @@ public class GitBrowseCommand extends AbstractGitCommand } } - //~--- methods -------------------------------------------------------------- - private FileObject createEmtpyRoot() { FileObject fileObject = new FileObject(); fileObject.setName(""); @@ -166,18 +152,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 { @@ -221,97 +195,41 @@ public class GitBrowseCommand extends AbstractGitCommand 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( - TreeFilter.ANY_DIFF - , - PathFilter.create(path) - ) - ); - - 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 = createEmtpyRoot(); 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) { @@ -368,7 +286,6 @@ 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) @@ -443,9 +360,9 @@ public class GitBrowseCommand extends AbstractGitCommand @Override public void accept(SyncAsyncExecutor.ExecutionType executionType) { logger.trace("fetch last commit for {} at {}", path, revId.getName()); - RevCommit commit = GitBrowseCommand.this.getLatestCommit(repo, revId, path); + Optional commit = getLatestCommit(repo, revId, path); - Optional lfsPointer = getLfsPointer(commit); + Optional lfsPointer = commit.flatMap(this::getLfsPointer); if (lfsPointer.isPresent()) { setFileLengthFromLfsBlob(lfsPointer.get()); @@ -454,13 +371,28 @@ public class GitBrowseCommand extends AbstractGitCommand } file.setPartialResult(false); - if (commit != null) { - applyValuesFromCommit(executionType, commit); + if (commit.isPresent()) { + applyValuesFromCommit(executionType, commit.get()); } else { logger.warn("could not find latest commit for {} on {}", path, revId); } } + 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 setFileLengthFromLfsBlob(LfsPointer lfsPointer) { BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); String oid = lfsPointer.getOid().getName(); @@ -477,19 +409,16 @@ public class GitBrowseCommand extends AbstractGitCommand file.setLastModified(GitUtil.getCommitTime(commit)); file.setDescription(commit.getShortMessage()); if (executionType == ASYNCHRONOUS && browserResult != null) { - request.updateCache(browserResult); - logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); + updateCache(request); } } private Optional getLfsPointer(RevCommit commit) { - Optional lfsPointer; try { - lfsPointer = commit == null ? empty() : GitUtil.getLfsPointer(repo, path, commit, treeWalk); + return GitUtil.getLfsPointer(repo, path, commit, treeWalk); } catch (IOException e) { throw new InternalRepositoryException(repository, "could not read lfs pointer", e); } - return lfsPointer; } } @@ -506,8 +435,7 @@ public class GitBrowseCommand extends AbstractGitCommand public void run() { file.setPartialResult(false); file.setComputationAborted(true); - request.updateCache(browserResult); - logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); + updateCache(request); } } } From ee0972ef34adeb3149a7ba61db74f1cb23564d44 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 12 Dec 2019 17:31:55 +0100 Subject: [PATCH 19/57] synchronize cache updates --- .../scm/repository/spi/GitBrowseCommand.java | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) 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 7927d38b2c..75c8631975 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 @@ -96,6 +96,8 @@ public class GitBrowseCommand extends AbstractGitCommand */ private static final Logger logger = LoggerFactory.getLogger(GitBrowseCommand.class); + private static final Object asyncMonitor = new Object(); + private final LfsBlobStoreFactory lfsBlobStoreFactory; private final SyncAsyncExecutor executor; @@ -118,11 +120,6 @@ public class GitBrowseCommand extends AbstractGitCommand if (revId != null) { browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); - executor.execute(executionType -> { - if (executionType == ASYNCHRONOUS) { - updateCache(request); - } - }, () -> {}); return browserResult; } else { logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); @@ -364,17 +361,19 @@ public class GitBrowseCommand extends AbstractGitCommand Optional lfsPointer = commit.flatMap(this::getLfsPointer); - if (lfsPointer.isPresent()) { - setFileLengthFromLfsBlob(lfsPointer.get()); - } else { - file.setLength(loader.getSize()); - } + synchronized (asyncMonitor) { + if (lfsPointer.isPresent()) { + setFileLengthFromLfsBlob(lfsPointer.get()); + } else { + file.setLength(loader.getSize()); + } - file.setPartialResult(false); - if (commit.isPresent()) { - applyValuesFromCommit(executionType, commit.get()); - } else { - logger.warn("could not find latest commit for {} on {}", path, revId); + file.setPartialResult(false); + if (commit.isPresent()) { + applyValuesFromCommit(executionType, commit.get()); + } else { + logger.warn("could not find latest commit for {} on {}", path, revId); + } } } @@ -433,9 +432,17 @@ public class GitBrowseCommand extends AbstractGitCommand @Override public void run() { - file.setPartialResult(false); - file.setComputationAborted(true); - updateCache(request); + synchronized (asyncMonitor) { + if (browserResult.getFile().getChildren().stream().anyMatch(FileObject::isPartialResult)) { + browserResult.getFile().getChildren().stream().filter(FileObject::isPartialResult).forEach( + f -> { + f.setPartialResult(false); + f.setComputationAborted(true); + } + ); + updateCache(request); + } + } } } } From c8a115a37352ec2537e2575e1468b2469e451425 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Dec 2019 08:20:26 +0100 Subject: [PATCH 20/57] Mark files aborted recursively --- .../scm/repository/spi/GitBrowseCommand.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) 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 75c8631975..c95fa13d95 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 @@ -185,7 +185,7 @@ public class GitBrowseCommand extends AbstractGitCommand file.setPartialResult(true); executor.execute( new CompleteFileInformation(path, revId, repo, treeWalk, file, loader, request), - new AbortFileInformation(file, request) + new AbortFileInformation(request) ); } } @@ -422,27 +422,32 @@ public class GitBrowseCommand extends AbstractGitCommand } private class AbortFileInformation implements Runnable { - private final FileObject file; private final BrowseCommandRequest request; - public AbortFileInformation(FileObject file, BrowseCommandRequest request) { - this.file = file; + public AbortFileInformation(BrowseCommandRequest request) { this.request = request; } @Override public void run() { synchronized (asyncMonitor) { - if (browserResult.getFile().getChildren().stream().anyMatch(FileObject::isPartialResult)) { - browserResult.getFile().getChildren().stream().filter(FileObject::isPartialResult).forEach( - f -> { - f.setPartialResult(false); - f.setComputationAborted(true); - } - ); + 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; + } } } From 0374aad7a749764d04ee19570aec9f8e5ca3edc4 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 13 Dec 2019 13:46:20 +0100 Subject: [PATCH 21/57] DefaultSyncAsyncExecutorProvider should be a Singleton --- .../scm/repository/DefaultSyncAsyncExecutorProvider.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java index 4c1f0429da..1727fff970 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java @@ -3,12 +3,15 @@ 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; @@ -20,6 +23,7 @@ public class DefaultSyncAsyncExecutorProvider implements SyncAsyncExecutorProvid private final ExecutorService executor; private final int defaultMaxAsyncAbortSeconds; + @Inject public DefaultSyncAsyncExecutorProvider() { this(Executors.newFixedThreadPool(getProperty(NUMBER_OF_THREADS_PROPERTY, DEFAULT_NUMBER_OF_THREADS))); } @@ -30,7 +34,7 @@ public class DefaultSyncAsyncExecutorProvider implements SyncAsyncExecutorProvid } public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds) { - return createExecutorWithSecondsToTimeout(switchToAsyncInSeconds, DEFAULT_MAX_ASYNC_ABORT_SECONDS); + return createExecutorWithSecondsToTimeout(switchToAsyncInSeconds, defaultMaxAsyncAbortSeconds); } public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds, int maxAsyncAbortSeconds) { From 8410e7e679dcb160ce05edae9069e3efcaf59071 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 13 Dec 2019 13:52:08 +0100 Subject: [PATCH 22/57] Log duration of commit message calculation and do not synchronize over all instances --- .../scm/repository/spi/GitBrowseCommand.java | 69 ++++++++----------- 1 file changed, 27 insertions(+), 42 deletions(-) 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 c95fa13d95..b0ec131b9c 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,6 +35,7 @@ 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; @@ -94,9 +95,12 @@ public class GitBrowseCommand extends AbstractGitCommand /** * the logger for GitBrowseCommand */ - private static final Logger logger = - LoggerFactory.getLogger(GitBrowseCommand.class); - private static final Object asyncMonitor = new Object(); + 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; @@ -123,12 +127,11 @@ public class GitBrowseCommand extends AbstractGitCommand return browserResult; } else { logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); - return new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); + return new BrowserResult(Constants.HEAD, request.getRevision(), createEmptyRoot()); } } private ObjectId computeRevIdToBrowse(BrowseCommandRequest request, org.eclipse.jgit.lib.Repository repo) throws IOException { - if (Util.isEmpty(request.getRevision())) { return getDefaultBranch(repo); } else { @@ -141,7 +144,7 @@ public class GitBrowseCommand extends AbstractGitCommand } } - private FileObject createEmtpyRoot() { + private FileObject createEmptyRoot() { FileObject fileObject = new FileObject(); fileObject.setName(""); fileObject.setPath(""); @@ -198,7 +201,6 @@ public class GitBrowseCommand extends AbstractGitCommand } private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException { - try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo)) { logger.debug("load repository browser for revision {}", revId.name()); @@ -215,7 +217,7 @@ public class GitBrowseCommand extends AbstractGitCommand } if (isRootRequest(request)) { - FileObject result = createEmtpyRoot(); + FileObject result = createEmptyRoot(); findChildren(result, repo, request, revId, treeWalk); return result; } else { @@ -283,58 +285,36 @@ public class GitBrowseCommand extends AbstractGitCommand throw notFound(entity("File", request.getPath()).in("Revision", revId.getName()).in(this.repository)); } - 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()); - } - catch (NotFoundException ex) - { + return GitSubModuleParser.parse(baos.toString()); + } catch (NotFoundException ex) { logger.trace("could not find .gitmodules: {}", ex.getMessage()); - subRepositories = Collections.emptyMap(); + return Collections.emptyMap(); } - - 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 --------------------------------------------------------------- - - /** sub repository cache */ - private final Map> subrepositoryCache = Maps.newHashMap(); - private class CompleteFileInformation implements Consumer { private final String path; private final ObjectId revId; @@ -357,6 +337,9 @@ public class GitBrowseCommand extends AbstractGitCommand @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); Optional lfsPointer = commit.flatMap(this::getLfsPointer); @@ -375,6 +358,8 @@ public class GitBrowseCommand extends AbstractGitCommand 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, From 5e47ef0323cbc9445cf7177b3af2275f88b598aa Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Dec 2019 14:41:36 +0100 Subject: [PATCH 23/57] Compute LFS attributes on top commit In the previous version, the LFS attributes were read for the latest commit of the file. This is not the way, a git client handles LFS files. Therefore we switch to the way, the native git client works and read the attributes from the commit of the command. --- .../java/sonia/scm/repository/GitUtil.java | 4 ++ .../scm/repository/spi/GitBrowseCommand.java | 72 ++++++++++--------- 2 files changed, 42 insertions(+), 34 deletions(-) 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 b0ec131b9c..d13991558a 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 @@ -39,6 +39,7 @@ 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; @@ -50,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; @@ -186,8 +188,20 @@ public class GitBrowseCommand extends AbstractGitCommand if (!file.isDirectory() &&!request.isDisableLastCommit()) { 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()) { + setFileLengthFromLfsBlob(lfsPointer.get(), file); + } else { + file.setLength(loader.getSize()); + } + executor.execute( - new CompleteFileInformation(path, revId, repo, treeWalk, file, loader, request), + new CompleteFileInformation(path, revId, repo, file, request), new AbortFileInformation(request) ); } @@ -315,22 +329,40 @@ public class GitBrowseCommand extends AbstractGitCommand return null; } + private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) { + try { + Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit); + + 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(-1); + } 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 TreeWalk treeWalk; private final FileObject file; - private final ObjectLoader loader; private final BrowseCommandRequest request; - public CompleteFileInformation(String path, ObjectId revId, org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, FileObject file, ObjectLoader loader, 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.treeWalk = treeWalk; this.file = file; - this.loader = loader; this.request = request; } @@ -342,15 +374,7 @@ public class GitBrowseCommand extends AbstractGitCommand Optional commit = getLatestCommit(repo, revId, path); - Optional lfsPointer = commit.flatMap(this::getLfsPointer); - synchronized (asyncMonitor) { - if (lfsPointer.isPresent()) { - setFileLengthFromLfsBlob(lfsPointer.get()); - } else { - file.setLength(loader.getSize()); - } - file.setPartialResult(false); if (commit.isPresent()) { applyValuesFromCommit(executionType, commit.get()); @@ -377,18 +401,6 @@ public class GitBrowseCommand extends AbstractGitCommand } } - private void setFileLengthFromLfsBlob(LfsPointer lfsPointer) { - 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(-1); - } else { - file.setLength(blob.getSize()); - } - } - private void applyValuesFromCommit(SyncAsyncExecutor.ExecutionType executionType, RevCommit commit) { file.setLastModified(GitUtil.getCommitTime(commit)); file.setDescription(commit.getShortMessage()); @@ -396,14 +408,6 @@ public class GitBrowseCommand extends AbstractGitCommand updateCache(request); } } - - private Optional getLfsPointer(RevCommit commit) { - try { - return GitUtil.getLfsPointer(repo, path, commit, treeWalk); - } catch (IOException e) { - throw new InternalRepositoryException(repository, "could not read lfs pointer", e); - } - } } private class AbortFileInformation implements Runnable { From 7bd48c91bc35b5787f34aa5572a6972f906dc0ba Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Dec 2019 14:41:55 +0100 Subject: [PATCH 24/57] Fix error for empty repositories --- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7d2b8c94a7..7977d9f3e0 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -28,7 +28,7 @@ export function fetchSourcesWithoutOptionalLoadingState( .then(response => response.json()) .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, sources)); - if (sources._embedded.children && sources._embedded.children.find(c => c.partialResult)) { + if (sources._embedded && sources._embedded.children && sources._embedded.children.find(c => c.partialResult)) { setTimeout(() => dispatch(fetchSourcesWithoutOptionalLoadingState(repository, revision, path, false)), 3000); } }) From 4ef97816e0c61cb72c3338c263e8fc4e20a326f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 17 Dec 2019 10:25:53 +0100 Subject: [PATCH 25/57] Trigger update of sources from component The old trigger in the dispatcher function led to updates even when the component was no longer mounted (aka displayed). --- .../src/repos/sources/components/FileTree.tsx | 21 ++++++++- .../src/repos/sources/modules/sources.ts | 45 +++++++++++++------ 2 files changed, 50 insertions(+), 16 deletions(-) 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 150e4d1eb6..419171b301 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,6 +19,8 @@ type Props = WithTranslation & { path: string; baseUrl: string; + updateSources: () => void; + // context props match: any; }; @@ -40,6 +42,13 @@ export function findParent(path: string) { } class FileTree extends React.Component { + componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>, snapshot?: any): void { + const { tree, updateSources } = this.props; + if (tree?._embedded?.children && tree._embedded.children.find(c => c.partialResult)) { + setTimeout(updateSources, 3000); + } + } + render() { const { error, loading, tree } = this.props; @@ -123,6 +132,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 +158,5 @@ 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/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 7d2b8c94a7..e6ab2836b4 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -9,28 +9,29 @@ 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 fetchSourcesWithoutOptionalLoadingState(repository, revision, path, true); -} - -export function fetchSourcesWithoutOptionalLoadingState( +export function fetchSources( repository: Repository, revision: string, path: string, - dispatchLoading: boolean + initialLoad = true ) { - return function(dispatch: any) { - if (dispatchLoading) { + 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)) } return apiClient .get(createUrl(repository, revision, path)) .then(response => response.json()) .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, sources)); - if (sources._embedded.children && sources._embedded.children.find(c => c.partialResult)) { - setTimeout(() => dispatch(fetchSourcesWithoutOptionalLoadingState(repository, revision, path, false)), 3000); - } }) .catch(err => { dispatch(fetchSourcesFailure(repository, revision, path, err)); @@ -56,10 +57,17 @@ export function fetchSourcesPending(repository: Repository, revision: string, pa }; } +export function updateSourcesPending(repository: Repository, revision: string, path: string): Action { + return { + type: "UPDATE_PENDING", + 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) }; } @@ -91,6 +99,11 @@ export default function reducer( ...state, [action.itemId]: action.payload }; + } else if (action.itemId && action.type === "UPDATE_PENDING") { + return { + ...state, + [action.itemId]: { updatePending: true }} + ; } return state; } @@ -113,13 +126,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[createItemId(repository, revision, path)]?.updatePending; } export function getFetchSourcesFailure( From c72bcb04c1e62f769314352ffd1ef3850ca22c6a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 17 Dec 2019 12:14:43 +0100 Subject: [PATCH 26/57] Cancel update call is component will be unmounted --- .../src/repos/sources/components/FileTree.tsx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) 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 419171b301..7983fdd32e 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -25,6 +25,10 @@ type Props = WithTranslation & { match: any; }; +type State = { + stoppableUpdateHandler?: number; +} + const FixedWidthTh = styled.th` width: 16px; `; @@ -41,11 +45,26 @@ export function findParent(path: string) { return ""; } -class FileTree extends React.Component { - componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>, snapshot?: any): void { - const { tree, updateSources } = this.props; - if (tree?._embedded?.children && tree._embedded.children.find(c => c.partialResult)) { - setTimeout(updateSources, 3000); +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); } } From 7da7a6881739e504c671f23443535d258b63f226 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 17 Dec 2019 12:36:58 +0100 Subject: [PATCH 27/57] Prevent "flicker" of tree while loading update --- .../ui-webapp/src/repos/sources/modules/sources.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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 e6ab2836b4..f976928e9f 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -25,7 +25,7 @@ export function fetchSources( if (initialLoad) { dispatch(fetchSourcesPending(repository, revision, path)); } else { - dispatch(updateSourcesPending(repository, revision, path)) + dispatch(updateSourcesPending(repository, revision, path, getSources(state, repository, revision, path))); } return apiClient .get(createUrl(repository, revision, path)) @@ -57,9 +57,10 @@ export function fetchSourcesPending(repository: Repository, revision: string, pa }; } -export function updateSourcesPending(repository: Repository, revision: string, path: string): Action { +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) }; } @@ -94,16 +95,11 @@ 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 }; - } else if (action.itemId && action.type === "UPDATE_PENDING") { - return { - ...state, - [action.itemId]: { updatePending: true }} - ; } return state; } From f257a8eeb86f4c71edbc01ae62af6ac1fa40fb3f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 17 Dec 2019 12:42:05 +0100 Subject: [PATCH 28/57] Fix formatting --- .../src/repos/sources/components/FileTree.tsx | 12 +++++----- .../src/repos/sources/modules/sources.ts | 22 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) 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 7983fdd32e..fee6f985cd 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -27,7 +27,7 @@ type Props = WithTranslation & { type State = { stoppableUpdateHandler?: number; -} +}; const FixedWidthTh = styled.th` width: 16px; @@ -46,7 +46,6 @@ export function findParent(path: string) { } class FileTree extends React.Component { - constructor(props: Props) { super(props); this.state = {}; @@ -54,10 +53,10 @@ class FileTree extends React.Component { componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) { - const {tree, updateSources} = this.props; + 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}); + this.setState({ stoppableUpdateHandler: stoppableUpdateHandler }); } } } @@ -177,5 +176,8 @@ const mapStateToProps = (state: any, ownProps: Props) => { export default compose( withRouter, - connect(mapStateToProps, mapDispatchToProps) + connect( + mapStateToProps, + mapDispatchToProps + ) )(withTranslation("repos")(FileTree)); 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 f976928e9f..8af2cc33cc 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -9,16 +9,13 @@ 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, - initialLoad = true -) { +`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)) { + if ( + isFetchSourcesPending(state, repository, revision, path) || + isUpdateSourcePending(state, repository, revision, path) + ) { return; } @@ -57,10 +54,15 @@ export function fetchSourcesPending(repository: Repository, revision: string, pa }; } -export function updateSourcesPending(repository: Repository, revision: string, path: string, currentSources: any): Action { +export function updateSourcesPending( + repository: Repository, + revision: string, + path: string, + currentSources: any +): Action { return { type: "UPDATE_PENDING", - payload: { updatePending: true, sources: currentSources}, + payload: { updatePending: true, sources: currentSources }, itemId: createItemId(repository, revision, path) }; } From a61799723b834846120e7a5b17459f62615ccb9f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 17 Dec 2019 12:42:31 +0100 Subject: [PATCH 29/57] Fix typo --- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8af2cc33cc..8a04dcdc76 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -9,7 +9,7 @@ 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, initialLoad = true) { +export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true) { return function(dispatch: any, getState: () => any) { const state = getState(); if ( From 45b989cd1afc061e2395a98e38f1ab37111d082a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 17 Dec 2019 13:03:05 +0100 Subject: [PATCH 30/57] Always show file size The file size is always present and not updated in the background --- scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9190a02dd6..037d8f7c45 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx @@ -88,7 +88,7 @@ class FileTreeLeaf extends React.Component { {this.createFileIcon(file)} {this.createFileName(file)} - {this.contentIfPresent(file, fileSize)} + {fileSize} {this.contentIfPresent(file, )} {this.contentIfPresent(file, file.description)} From 5ea149c2622b1d4f8c4f4c4932f0b88dc9198b7d Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 17 Dec 2019 13:34:28 +0100 Subject: [PATCH 31/57] Adapt tests to new state layout --- .../src/repos/sources/modules/sources.test.ts | 18 +++++++++++------- .../src/repos/sources/modules/sources.ts | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) 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..fd676d7c57 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 @@ -127,7 +127,7 @@ describe("sources fetch", () => { { type: FETCH_SOURCES_SUCCESS, itemId: "scm/core/_/", - payload: collection + payload: { updatePending: false, sources: collection } } ]; @@ -148,7 +148,7 @@ describe("sources fetch", () => { { type: FETCH_SOURCES_SUCCESS, itemId: "scm/core/abc/src", - payload: collection + payload: { updatePending: false, sources: collection } } ]; @@ -182,14 +182,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 +200,7 @@ describe("selector tests", () => { const state = { sources: { "scm/core/abc/src/main/package.json": { - noDirectory + sources: {noDirectory} } } }; @@ -223,7 +223,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 +234,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 8a04dcdc76..8abdec9fd5 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -134,7 +134,7 @@ export function isFetchSourcesPending(state: any, repository: Repository, revisi } function isUpdateSourcePending(state: any, repository: Repository, revision: string, path: string): boolean { - return state?.sources[createItemId(repository, revision, path)]?.updatePending; + return state?.sources && state.sources[createItemId(repository, revision, path)]?.updatePending; } export function getFetchSourcesFailure( From dc6d9cc5eb8bbbff90a16e12fd231cfaa8f062c8 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 18 Dec 2019 11:03:07 +0100 Subject: [PATCH 32/57] return revision on merge --- .../repository/api/MergeCommandResult.java | 12 +++- .../java/sonia/scm/repository/spi/Differ.java | 70 ++++++++++--------- .../spi/GitFastForwardIfPossible.java | 3 +- .../scm/repository/spi/GitMergeCommit.java | 12 +++- .../scm/repository/spi/GitMergeStrategy.java | 6 +- .../repository/spi/GitMergeWithSquash.java | 9 ++- .../sonia/scm/repository/spi/GitMerger.java | 17 +++++ .../ui-components/src/repos/LoadingDiff.tsx | 12 +++- .../src/repos/changesets/ChangesetDiff.tsx | 2 +- scm-ui/ui-webapp/public/locales/de/repos.json | 2 +- scm-ui/ui-webapp/public/locales/en/repos.json | 2 +- 11 files changed, 97 insertions(+), 50 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java index 53f712cddc..54b57c668f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java @@ -13,17 +13,19 @@ import static java.util.Collections.unmodifiableCollection; */ public class MergeCommandResult { private final Collection filesWithConflict; + private static String mergeCommitRevision; - private MergeCommandResult(Collection filesWithConflict) { + public MergeCommandResult(Collection filesWithConflict, String mergeCommitRevision) { this.filesWithConflict = filesWithConflict; + this.mergeCommitRevision = mergeCommitRevision; } public static MergeCommandResult success() { - return new MergeCommandResult(emptyList()); + return new MergeCommandResult(emptyList(), mergeCommitRevision); } public static MergeCommandResult failure(Collection filesWithConflict) { - return new MergeCommandResult(new HashSet<>(filesWithConflict)); + return new MergeCommandResult(new HashSet<>(filesWithConflict), ""); } /** @@ -41,4 +43,8 @@ public class MergeCommandResult { public Collection getFilesWithConflict() { return unmodifiableCollection(filesWithConflict); } + + public String getMergeCommitRevision() { + return mergeCommitRevision; + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java index 0204ca4e3c..fdecac6314 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -2,6 +2,7 @@ package sonia.scm.repository.spi; import com.google.common.base.Strings; import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; @@ -10,6 +11,8 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.EmptyTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; +import sonia.scm.ContextEntry; +import sonia.scm.NotFoundException; import sonia.scm.repository.GitUtil; import sonia.scm.util.Util; @@ -35,49 +38,48 @@ final class Differ implements AutoCloseable { } private static Differ create(Repository repository, DiffCommandRequest request) throws IOException { - RevWalk walk = new RevWalk(repository); + RevWalk walk = new RevWalk(repository); - ObjectId revision = repository.resolve(request.getRevision()); - RevCommit commit = walk.parseCommit(revision); + ObjectId revision = repository.resolve(request.getRevision()); + if (revision == null) { + throw NotFoundException.notFound(ContextEntry.ContextBuilder.entity("revision not found", request.getRevision())); + } + RevCommit commit; + try { + commit = walk.parseCommit(revision); + } catch (MissingObjectException ex) { + throw NotFoundException.notFound(ContextEntry.ContextBuilder.entity("revision not found", request.getRevision())); + } - walk.markStart(commit); - commit = walk.next(); - TreeWalk treeWalk = new TreeWalk(repository); - treeWalk.reset(); - treeWalk.setRecursive(true); + walk.markStart(commit); + commit = walk.next(); + TreeWalk treeWalk = new TreeWalk(repository); + treeWalk.reset(); + treeWalk.setRecursive(true); - if (Util.isNotEmpty(request.getPath())) - { - treeWalk.setFilter(PathFilter.create(request.getPath())); - } + if (Util.isNotEmpty(request.getPath())) { + treeWalk.setFilter(PathFilter.create(request.getPath())); + } - if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) - { - ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); - ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision); - RevTree tree = walk.parseCommit(ancestorId).getTree(); + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) { + ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); + ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision); + RevTree tree = walk.parseCommit(ancestorId).getTree(); + treeWalk.addTree(tree); + } else if (commit.getParentCount() > 0) { + RevTree tree = commit.getParent(0).getTree(); + + if (tree != null) { treeWalk.addTree(tree); - } - else if (commit.getParentCount() > 0) - { - RevTree tree = commit.getParent(0).getTree(); - - if (tree != null) - { - treeWalk.addTree(tree); - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } - } - else - { + } else { treeWalk.addTree(new EmptyTreeIterator()); } + } else { + treeWalk.addTree(new EmptyTreeIterator()); + } - treeWalk.addTree(commit.getTree()); + treeWalk.addTree(commit.getTree()); return new Differ(commit, walk, treeWalk); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java index 64a20a33cb..ddca0a78b0 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java @@ -7,6 +7,7 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandResult; import java.io.IOException; +import java.util.Collections; class GitFastForwardIfPossible extends GitMergeStrategy { @@ -22,7 +23,7 @@ class GitFastForwardIfPossible extends GitMergeStrategy { MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); if (fastForwardResult.getMergeStatus().isSuccessful()) { push(); - return MergeCommandResult.success(); + return new MergeCommandResult(Collections.emptyList(), ""); } else { return fallbackMerge.run(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java index 6aa68a0ea8..f527cff900 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -3,10 +3,17 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.revwalk.RevCommit; +import sonia.scm.ContextEntry; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandResult; import java.io.IOException; +import java.util.Collections; +import java.util.Optional; + +import static sonia.scm.repository.spi.GitMerger.evaluateRevisionFromMergeCommit; class GitMergeCommit extends GitMergeStrategy { @@ -21,11 +28,12 @@ class GitMergeCommit extends GitMergeStrategy { MergeResult result = doMergeInClone(mergeCommand); if (result.getMergeStatus().isSuccessful()) { - doCommit(); + Optional revCommit = doCommit(); push(); - return MergeCommandResult.success(); + return new MergeCommandResult(Collections.emptyList(), evaluateRevisionFromMergeCommit(revCommit)); } else { return analyseFailure(result); } } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java index 1d53b99c99..17b4571820 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -6,6 +6,7 @@ import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.InternalRepositoryException; @@ -14,6 +15,7 @@ import sonia.scm.repository.api.MergeCommandResult; import java.io.IOException; import java.text.MessageFormat; +import java.util.Optional; abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker { @@ -52,9 +54,9 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker doCommit() { logger.debug("merged branch {} into {}", toMerge, target); - doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author); + return doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author); } private String determineMessageTemplate() { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java index b688956404..a1e921e8a9 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java @@ -2,11 +2,16 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.revwalk.RevCommit; import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandResult; import org.eclipse.jgit.api.MergeCommand; import java.io.IOException; +import java.util.Collections; +import java.util.Optional; + +import static sonia.scm.repository.spi.GitMerger.evaluateRevisionFromMergeCommit; class GitMergeWithSquash extends GitMergeStrategy { @@ -21,9 +26,9 @@ class GitMergeWithSquash extends GitMergeStrategy { MergeResult result = doMergeInClone(mergeCommand); if (result.getMergeStatus().isSuccessful()) { - doCommit(); + Optional revCommit = doCommit(); push(); - return MergeCommandResult.success(); + return new MergeCommandResult(Collections.emptyList(), evaluateRevisionFromMergeCommit(revCommit)); } else { return analyseFailure(result); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java new file mode 100644 index 0000000000..87334e5a57 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java @@ -0,0 +1,17 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.revwalk.RevCommit; +import sonia.scm.ContextEntry; +import sonia.scm.repository.InternalRepositoryException; + +import java.util.Optional; + +public class GitMerger { + + static String evaluateRevisionFromMergeCommit(Optional revCommit) { + if (revCommit.isPresent()) { + return revCommit.get().toString().split(" ")[1]; + } + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity(GitMergeCommit.class, "merge commit failed"), "could not create commit on merge"); + } +} diff --git a/scm-ui/ui-components/src/repos/LoadingDiff.tsx b/scm-ui/ui-components/src/repos/LoadingDiff.tsx index 45b62f9b35..ec782bc968 100644 --- a/scm-ui/ui-components/src/repos/LoadingDiff.tsx +++ b/scm-ui/ui-components/src/repos/LoadingDiff.tsx @@ -7,8 +7,11 @@ import parser from "gitdiff-parser"; import Loading from "../Loading"; import Diff from "./Diff"; import { DiffObjectProps, File } from "./DiffTypes"; +import { NotFoundError } from "../errors"; +import { Notification } from "../index"; +import {withTranslation, WithTranslation} from "react-i18next"; -type Props = DiffObjectProps & { +type Props = WithTranslation & DiffObjectProps & { url: string; defaultCollapse?: boolean; }; @@ -43,7 +46,7 @@ class LoadingDiff extends React.Component { fetchDiff = () => { const { url } = this.props; - this.setState({loading: true}); + this.setState({ loading: true }); apiClient .get(url) .then(response => response.text()) @@ -66,6 +69,9 @@ class LoadingDiff extends React.Component { render() { const { diff, loading, error } = this.state; if (error) { + if (error instanceof NotFoundError) { + return {this.props.t("changesets.noChangesets")}; + } return ; } else if (loading) { return ; @@ -77,4 +83,4 @@ class LoadingDiff extends React.Component { } } -export default LoadingDiff; +export default withTranslation("repos")(LoadingDiff); diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx index 78f4e90cb3..1789cb5517 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetDiff.tsx @@ -28,7 +28,7 @@ class ChangesetDiff extends React.Component { return {t("changeset.diffNotSupported")}; } else { const url = this.createUrl(changeset); - return ; + return ; } } } diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index df6c534ece..83a7918018 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -72,7 +72,7 @@ "changesets": { "errorTitle": "Fehler", "errorSubtitle": "Changesets konnten nicht abgerufen werden", - "noChangesets": "Keine Changesets in diesem Branch gefunden.", + "noChangesets": "Keine Changesets in diesem Branch gefunden. Die Commits könnten gelöscht wurden sein.", "branchSelectorLabel": "Branches", "collapseDiffs": "Auf-/Zuklappen" }, diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 103e30b825..eaeba49e16 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -72,7 +72,7 @@ "changesets": { "errorTitle": "Error", "errorSubtitle": "Could not fetch changesets", - "noChangesets": "No changesets found for this branch.", + "noChangesets": "No changesets found for this branch. The commits could have been removed.", "branchSelectorLabel": "Branches", "collapseDiffs": "Collapse" }, From 8fa43da1eaceee32efcc75d7345129796ddc4379 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 18 Dec 2019 11:29:59 +0100 Subject: [PATCH 33/57] fixed typescript optional chaining with eslint and prettier --- scm-ui/eslint-config/package.json | 4 +- scm-ui/ui-components/package.json | 2 - scm-ui/ui-webapp/package.json | 2 - yarn.lock | 414 ++++-------------------------- 4 files changed, 46 insertions(+), 376 deletions(-) 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-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/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== From cf9d1edb701a868e5125fa9ff1573698058c1472 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 18 Dec 2019 11:48:17 +0100 Subject: [PATCH 34/57] Make partial file attributes explicit. --- .../java/sonia/scm/repository/FileObject.java | 140 +++++++++++++----- .../scm/repository/spi/GitBrowseCommand.java | 4 +- .../repository/spi/GitBrowseCommandTest.java | 34 ++--- .../spi/javahg/HgFileviewCommand.java | 4 +- .../repository/spi/HgBrowseCommandTest.java | 24 +-- .../scm/repository/spi/SvnBrowseCommand.java | 2 +- .../repository/spi/SvnBrowseCommandTest.java | 24 +-- scm-ui/ui-types/src/Sources.ts | 4 +- .../src/repos/sources/components/FileTree.tsx | 2 +- .../repos/sources/components/FileTreeLeaf.tsx | 17 ++- .../src/repos/sources/containers/Content.tsx | 2 +- .../src/repos/sources/modules/sources.test.ts | 4 +- .../BrowserResultToFileObjectDtoMapper.java | 11 ++ .../scm/api/v2/resources/FileObjectDto.java | 8 +- ...rowserResultToFileObjectDtoMapperTest.java | 9 +- .../v2/resources/SourceRootResourceTest.java | 4 +- 16 files changed, 190 insertions(+), 103 deletions(-) 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 acb7559094..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,10 +246,38 @@ 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; } @@ -255,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; } /** @@ -271,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; } @@ -310,30 +362,47 @@ public class FileObject implements LastModifiedAware, Serializable this.subRepository = subRepository; } + /** + * 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; } - public Collection getChildren() { - return unmodifiableCollection(children); - } - + /** + * 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 */ @@ -342,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; @@ -354,13 +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-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 d13991558a..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 @@ -345,7 +345,7 @@ public class GitBrowseCommand extends AbstractGitCommand 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); + file.setLength(null); } else { file.setLength(blob.getSize()); } @@ -402,7 +402,7 @@ public class GitBrowseCommand extends AbstractGitCommand } private void applyValuesFromCommit(SyncAsyncExecutor.ExecutionType executionType, RevCommit commit) { - file.setLastModified(GitUtil.getCommitTime(commit)); + file.setCommitDate(GitUtil.getCommitTime(commit)); file.setDescription(commit.getShortMessage()); if (executionType == ASYNCHRONOUS && browserResult != null) { updateCache(request); 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 9b45cda16f..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 @@ -108,9 +108,9 @@ 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()); @@ -132,28 +132,28 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { FileObject b = findFile(foList, "b.txt"); assertTrue(a.isPartialResult()); - assertNull("expected empty name before commit could have been read", a.getDescription()); - assertNull("expected empty date before commit could have been read", a.getLastModified()); + 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()); - assertNull("expected empty name before commit could have been read", b.getDescription()); - assertNull("expected empty date before commit could have been read", b.getLastModified()); + 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()); - assertNotNull("expected correct date after commit could have been read", a.getLastModified()); + assertTrue("expected correct date after commit could have been read", a.getCommitDate().isPresent()); assertTrue(b.isPartialResult()); - assertNull("expected empty name before commit could have been read", b.getDescription()); - assertNull("expected empty date before commit could have been read", b.getLastModified()); + 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()); - assertNotNull("expected correct date after commit could have been read", b.getLastModified()); + assertTrue("expected correct date after commit could have been read", b.getCommitDate().isPresent()); } } @@ -175,16 +175,16 @@ 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 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-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index f5fca71d65..dce6947622 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -13,8 +13,8 @@ export type File = { directory: boolean; description?: string; revision: string; - length: number; - lastModified?: string; + length?: number; + commitDate?: string; subRepository?: SubRepository; // TODO partialResult: boolean; computationAborted: boolean; 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 9782b69e0d..c1384b5192 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -133,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") && } 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 8a84fc5e1e..cd6baea395 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx @@ -64,9 +64,11 @@ class FileTreeLeaf extends React.Component { return {file.name}; }; - contentIfPresent = (file: File, content: any) => { + contentIfPresent = (file: File, attribute: string, content: (file: File) => any) => { const { t } = this.props; - if (file.computationAborted) { + if (file.hasOwnProperty(attribute)) { + return content(file); + } else if (file.computationAborted) { return ( @@ -79,23 +81,24 @@ class FileTreeLeaf extends React.Component { ); } else { - return content; + 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} - {this.contentIfPresent(file, )} + {file.directory ? "" : this.contentIfPresent(file, "length", renderFileSize)} + {this.contentIfPresent(file, "commitDate", renderCommitDate)} - {this.contentIfPresent(file, file.description)} + {this.contentIfPresent(file, "description", file => file.description)} {binder.hasExtension("repos.sources.tree.row.right") && ( 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 fd676d7c57..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: { 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 866c2cc0b9..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 @@ -15,6 +15,9 @@ 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 { @@ -39,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 4676a0fb03..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,10 +21,10 @@ 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) 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 27ef82834b..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 @@ -30,6 +30,7 @@ public class BrowserResultToFileObjectDtoMapperTest { private FileObject fileObject1 = new FileObject(); private FileObject fileObject2 = new FileObject(); + private FileObject partialFileObject = new FileObject(); @Before @@ -42,15 +43,15 @@ public class BrowserResultToFileObjectDtoMapperTest { 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/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 7b205c732a..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 @@ -122,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(); @@ -131,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; From 42d8d844c0b616cbcbd9e7a88f5927843865d93d Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 18 Dec 2019 11:52:08 +0100 Subject: [PATCH 35/57] Remove unused functions --- .../scm/repository/api/BrowseCommandBuilder.java | 4 ---- .../scm/repository/spi/FileBaseCommandRequest.java | 11 +---------- 2 files changed, 1 insertion(+), 14 deletions(-) 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 437e3f5fa0..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 @@ -300,10 +300,6 @@ public final class BrowseCommandBuilder return this; } - public void setComputationTimeoutMilliSeconds(long computationTimeoutMilliSeconds) { - request.setComputationTimeoutMilliSeconds(computationTimeoutMilliSeconds); - } - private void updateCache(BrowserResult updatedResult) { if (!disableCache) { CacheKey key = new CacheKey(repository, request); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index c6683905dd..0455e65a95 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -147,10 +147,6 @@ public abstract class FileBaseCommandRequest this.revision = revision; } - public void setComputationTimeoutMilliSeconds(long computationTimeoutMilliSeconds) { - this.computationTimeoutMilliSeconds = computationTimeoutMilliSeconds; - } - //~--- get methods ---------------------------------------------------------- /** @@ -179,10 +175,7 @@ public abstract class FileBaseCommandRequest return disableCommitValues; } - public long getComputationTimeoutMilliSeconds() { - return computationTimeoutMilliSeconds; - } -//~--- methods -------------------------------------------------------------- + //~--- methods -------------------------------------------------------------- /** * Method description @@ -221,6 +214,4 @@ public abstract class FileBaseCommandRequest private String revision; private boolean disableCommitValues = false; - - private long computationTimeoutMilliSeconds = 1000; } From a5dc2828ad05077104970161c2e4e70015f2b573 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 18 Dec 2019 12:11:57 +0100 Subject: [PATCH 36/57] Remove unused function --- .../sonia/scm/repository/spi/FileBaseCommandRequest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index 0455e65a95..9f563345fd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -171,10 +171,6 @@ public abstract class FileBaseCommandRequest return revision; } - public boolean isDisableCommitValues() { - return disableCommitValues; - } - //~--- methods -------------------------------------------------------------- /** @@ -212,6 +208,4 @@ public abstract class FileBaseCommandRequest /** Field description */ private String revision; - - private boolean disableCommitValues = false; } From edcc036f62b1ec4d135231b99a3088fcf83ad969 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 18 Dec 2019 12:48:11 +0100 Subject: [PATCH 37/57] Make reverse channel transient --- .../java/sonia/scm/repository/spi/BrowseCommandRequest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0c82c71331..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 @@ -249,5 +249,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** browse file objects recursive */ private boolean recursive = false; - private final Consumer updater; + // 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; } From 83cc1c4a6ff2a71eb207509649da6efed5d57532 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 18 Dec 2019 13:17:30 +0100 Subject: [PATCH 38/57] return revision on merge --- .../repository/api/MergeCommandResult.java | 22 +++++++------------ .../spi/GitFastForwardIfPossible.java | 2 +- .../scm/repository/spi/GitMergeStrategy.java | 2 +- .../repository/spi/GitMergeWithSquash.java | 4 ++-- .../sonia/scm/repository/spi/GitMerger.java | 3 ++- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java index 54b57c668f..5680416925 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java @@ -1,9 +1,7 @@ package sonia.scm.repository.api; import java.util.Collection; -import java.util.HashSet; -import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableCollection; /** @@ -13,19 +11,15 @@ import static java.util.Collections.unmodifiableCollection; */ public class MergeCommandResult { private final Collection filesWithConflict; - private static String mergeCommitRevision; + private String newHeadRevision; - public MergeCommandResult(Collection filesWithConflict, String mergeCommitRevision) { + public MergeCommandResult(Collection filesWithConflict) { this.filesWithConflict = filesWithConflict; - this.mergeCommitRevision = mergeCommitRevision; } - public static MergeCommandResult success() { - return new MergeCommandResult(emptyList(), mergeCommitRevision); - } - - public static MergeCommandResult failure(Collection filesWithConflict) { - return new MergeCommandResult(new HashSet<>(filesWithConflict), ""); + public MergeCommandResult(Collection filesWithConflict, String newHeadRevision) { + this.filesWithConflict = filesWithConflict; + this.newHeadRevision = newHeadRevision; } /** @@ -33,7 +27,7 @@ public class MergeCommandResult { * merge conflicts. In this case you can use {@link #getFilesWithConflict()} to check what files could not be merged. */ public boolean isSuccess() { - return filesWithConflict.isEmpty(); + return filesWithConflict.isEmpty() && newHeadRevision != null; } /** @@ -44,7 +38,7 @@ public class MergeCommandResult { return unmodifiableCollection(filesWithConflict); } - public String getMergeCommitRevision() { - return mergeCommitRevision; + public String getNewHeadRevision() { + return newHeadRevision; } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java index ddca0a78b0..cf03e832a3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java @@ -23,7 +23,7 @@ class GitFastForwardIfPossible extends GitMergeStrategy { MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); if (fastForwardResult.getMergeStatus().isSuccessful()) { push(); - return new MergeCommandResult(Collections.emptyList(), ""); + return new MergeCommandResult(Collections.emptyList(), fastForwardResult.getNewHead().toString()); } else { return fallbackMerge.run(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java index 17b4571820..16048c1ef6 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -69,6 +69,6 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker Date: Wed, 18 Dec 2019 13:28:22 +0000 Subject: [PATCH 39/57] Close branch feature/git_browse_performance From 3ea788c3e3e045f61a2ea9b8320d2ba66457fd16 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 18 Dec 2019 14:33:50 +0100 Subject: [PATCH 40/57] fix unit test --- .../src/main/java/sonia/scm/repository/spi/GitMerger.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java index 6fe942ed7e..5bca7440d2 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java @@ -1,9 +1,6 @@ package sonia.scm.repository.spi; -import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.revwalk.RevCommit; -import sonia.scm.ContextEntry; -import sonia.scm.repository.InternalRepositoryException; import java.util.Optional; @@ -13,6 +10,6 @@ public class GitMerger { if (revCommit.isPresent()) { return revCommit.get().toString().split(" ")[1]; } - throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity(GitMerger.class, "merge commit failed"), "could not create commit on merge"); + return ""; } } From c5f73931165394bff068e73d73030f9c6a7487a0 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 18 Dec 2019 14:38:09 +0100 Subject: [PATCH 41/57] rename helper class --- .../main/java/sonia/scm/repository/spi/GitMergeCommit.java | 6 ++---- .../java/sonia/scm/repository/spi/GitMergeWithSquash.java | 4 ++-- .../spi/{GitMerger.java => GitRevisionExtractor.java} | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) rename scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/{GitMerger.java => GitRevisionExtractor.java} (66%) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java index f527cff900..3c76ae1631 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -4,8 +4,6 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.revwalk.RevCommit; -import sonia.scm.ContextEntry; -import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandResult; @@ -13,7 +11,7 @@ import java.io.IOException; import java.util.Collections; import java.util.Optional; -import static sonia.scm.repository.spi.GitMerger.evaluateRevisionFromMergeCommit; +import static sonia.scm.repository.spi.GitRevisionExtractor.extractRevisionFromRevCommit; class GitMergeCommit extends GitMergeStrategy { @@ -30,7 +28,7 @@ class GitMergeCommit extends GitMergeStrategy { if (result.getMergeStatus().isSuccessful()) { Optional revCommit = doCommit(); push(); - return new MergeCommandResult(Collections.emptyList(), evaluateRevisionFromMergeCommit(revCommit)); + return new MergeCommandResult(Collections.emptyList(), extractRevisionFromRevCommit(revCommit)); } else { return analyseFailure(result); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java index d50ff1737d..44a690cbd0 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java @@ -11,7 +11,7 @@ import java.io.IOException; import java.util.Collections; import java.util.Optional; -import static sonia.scm.repository.spi.GitMerger.evaluateRevisionFromMergeCommit; +import static sonia.scm.repository.spi.GitRevisionExtractor.extractRevisionFromRevCommit; class GitMergeWithSquash extends GitMergeStrategy { @@ -28,7 +28,7 @@ class GitMergeWithSquash extends GitMergeStrategy { if (result.getMergeStatus().isSuccessful()) { Optional revCommit = doCommit(); push(); - return new MergeCommandResult(Collections.emptyList(), evaluateRevisionFromMergeCommit(revCommit)); + return new MergeCommandResult(Collections.emptyList(), extractRevisionFromRevCommit(revCommit)); } else { return analyseFailure(result); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java similarity index 66% rename from scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java rename to scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java index 5bca7440d2..6db9dea066 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMerger.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java @@ -4,9 +4,9 @@ import org.eclipse.jgit.revwalk.RevCommit; import java.util.Optional; -public class GitMerger { +public class GitRevisionExtractor { - static String evaluateRevisionFromMergeCommit(Optional revCommit) { + static String extractRevisionFromRevCommit(Optional revCommit) { if (revCommit.isPresent()) { return revCommit.get().toString().split(" ")[1]; } From ba6f8e5b4add69ef6c47d2a329503031d908b49b Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 18 Dec 2019 16:04:16 +0100 Subject: [PATCH 42/57] Small typo --- scm-ui/ui-webapp/public/locales/de/repos.json | 2 +- scm-ui/ui-webapp/public/locales/es/repos.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 83a7918018..3ed9804fe3 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -72,7 +72,7 @@ "changesets": { "errorTitle": "Fehler", "errorSubtitle": "Changesets konnten nicht abgerufen werden", - "noChangesets": "Keine Changesets in diesem Branch gefunden. Die Commits könnten gelöscht wurden sein.", + "noChangesets": "Keine Changesets in diesem Branch gefunden. Die Commits könnten gelöscht worden sein.", "branchSelectorLabel": "Branches", "collapseDiffs": "Auf-/Zuklappen" }, diff --git a/scm-ui/ui-webapp/public/locales/es/repos.json b/scm-ui/ui-webapp/public/locales/es/repos.json index 3b2451cceb..4e18669287 100644 --- a/scm-ui/ui-webapp/public/locales/es/repos.json +++ b/scm-ui/ui-webapp/public/locales/es/repos.json @@ -72,7 +72,7 @@ "changesets": { "errorTitle": "Error", "errorSubtitle": "No se han podido recuperar los changesets", - "noChangesets": "No se han encontrado changesets para esta rama branch.", + "noChangesets": "No se han encontrado changesets para esta rama branch. Los commits podrían haber sido eliminados.", "branchSelectorLabel": "Ramas", "collapseDiffs": "Colapso" }, From 39b4c59d82614726644fd60fd439e245de7db07b Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 19 Dec 2019 08:25:28 +0100 Subject: [PATCH 43/57] add unit tests --- .../spi/GitRevisionExtractorTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java new file mode 100644 index 0000000000..514d777f2a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java @@ -0,0 +1,30 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GitRevisionExtractorTest { + + @Test + void shouldReturnRevisionFromRevCommit() { + RevCommit revCommit = mock(RevCommit.class); + Optional optionalRevCommit = Optional.of(revCommit); + when(revCommit.toString()).thenReturn("commit 123456abcdef -t 4561"); + String revision = GitRevisionExtractor.extractRevisionFromRevCommit(optionalRevCommit); + assertThat(revision).isEqualTo("123456abcdef"); + } + + @Test + void shouldReturnEmptyStringIfRevCommitNotAvailable() { + Optional optionalRevCommit = Optional.empty(); + String revision = GitRevisionExtractor.extractRevisionFromRevCommit(optionalRevCommit); + assertThat(revision).isEqualTo(""); + } + +} From 1d3e51ce7b911edde833981034809b55e9db8e82 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 19 Dec 2019 09:27:18 +0100 Subject: [PATCH 44/57] Use static create methods instead of ambiguous constructor --- .../repository/api/MergeCommandResult.java | 19 +++++++++++++------ .../spi/GitFastForwardIfPossible.java | 2 +- .../scm/repository/spi/GitMergeCommit.java | 2 +- .../scm/repository/spi/GitMergeStrategy.java | 2 +- .../repository/spi/GitMergeWithSquash.java | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java index 5680416925..6a8fd8ab0d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java @@ -1,7 +1,9 @@ package sonia.scm.repository.api; import java.util.Collection; +import java.util.HashSet; +import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableCollection; /** @@ -10,18 +12,23 @@ import static java.util.Collections.unmodifiableCollection; * case you can use {@link #getFilesWithConflict()} to get a list of files with merge conflicts. */ public class MergeCommandResult { + private final Collection filesWithConflict; - private String newHeadRevision; + private final String newHeadRevision; - public MergeCommandResult(Collection filesWithConflict) { - this.filesWithConflict = filesWithConflict; - } - - public MergeCommandResult(Collection filesWithConflict, String newHeadRevision) { + private MergeCommandResult(Collection filesWithConflict, String newHeadRevision) { this.filesWithConflict = filesWithConflict; this.newHeadRevision = newHeadRevision; } + public static MergeCommandResult success(String newHeadRevision) { + return new MergeCommandResult(emptyList(), newHeadRevision); + } + + public static MergeCommandResult failure(Collection filesWithConflict) { + return new MergeCommandResult(new HashSet<>(filesWithConflict), null); + } + /** * If this returns true, the merge was successfull. If this returns false there were * merge conflicts. In this case you can use {@link #getFilesWithConflict()} to check what files could not be merged. diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java index cf03e832a3..85ba8c2f71 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java @@ -23,7 +23,7 @@ class GitFastForwardIfPossible extends GitMergeStrategy { MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); if (fastForwardResult.getMergeStatus().isSuccessful()) { push(); - return new MergeCommandResult(Collections.emptyList(), fastForwardResult.getNewHead().toString()); + return MergeCommandResult.success(fastForwardResult.getNewHead().toString()); } else { return fallbackMerge.run(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java index 3c76ae1631..29e404db41 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -28,7 +28,7 @@ class GitMergeCommit extends GitMergeStrategy { if (result.getMergeStatus().isSuccessful()) { Optional revCommit = doCommit(); push(); - return new MergeCommandResult(Collections.emptyList(), extractRevisionFromRevCommit(revCommit)); + return MergeCommandResult.success(extractRevisionFromRevCommit(revCommit)); } else { return analyseFailure(result); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java index 16048c1ef6..17b4571820 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -69,6 +69,6 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker revCommit = doCommit(); push(); - return new MergeCommandResult(Collections.emptyList(), extractRevisionFromRevCommit(revCommit)); + return MergeCommandResult.success(extractRevisionFromRevCommit(revCommit)); } else { return analyseFailure(result); } From f7c4894fb2c428fe40f63c58f5bccffef8641213 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 19 Dec 2019 09:47:31 +0100 Subject: [PATCH 45/57] Let merges without effect throw an exception --- .../java/sonia/scm/repository/spi/MergeCommand.java | 6 ++++++ .../scm/repository/spi/AbstractGitCommand.java | 4 ++++ .../sonia/scm/repository/spi/GitMergeCommit.java | 3 ++- .../scm/repository/spi/GitMergeWithSquash.java | 3 ++- .../scm/repository/spi/GitRevisionExtractor.java | 7 ++----- .../scm/repository/spi/GitMergeCommandTest.java | 13 ++++--------- .../repository/spi/GitRevisionExtractorTest.java | 11 +---------- 7 files changed, 21 insertions(+), 26 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java index a62e373dca..79d6b12c25 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommand.java @@ -7,6 +7,12 @@ import sonia.scm.repository.api.MergeStrategy; import java.util.Set; public interface MergeCommand { + /** + * Executes the merge. + * @param request The parameters specifying the merge. + * @return Result holding either the new revision or a list of conflicting files. + * @throws sonia.scm.NoChangesMadeException If the merge neither had a conflict nor made any change. + */ MergeCommandResult merge(MergeCommandRequest request); MergeDryRunCommandResult dryRun(MergeCommandRequest request); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index adf7878221..1c807c6fc7 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -186,6 +186,10 @@ class AbstractGitCommand return context; } + sonia.scm.repository.Repository getRepository() { + return repository; + } + void checkOutBranch(String branchName) throws IOException { try { clone.checkout().setName(branchName).call(); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java index 29e404db41..12bbe5f5b1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -4,6 +4,7 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.revwalk.RevCommit; +import sonia.scm.NoChangesMadeException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandResult; @@ -26,7 +27,7 @@ class GitMergeCommit extends GitMergeStrategy { MergeResult result = doMergeInClone(mergeCommand); if (result.getMergeStatus().isSuccessful()) { - Optional revCommit = doCommit(); + RevCommit revCommit = doCommit().orElseThrow(() -> new NoChangesMadeException(getRepository())); push(); return MergeCommandResult.success(extractRevisionFromRevCommit(revCommit)); } else { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java index e9d48dee3a..2a04e32c64 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java @@ -4,6 +4,7 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.revwalk.RevCommit; +import sonia.scm.NoChangesMadeException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandResult; @@ -26,7 +27,7 @@ class GitMergeWithSquash extends GitMergeStrategy { MergeResult result = doMergeInClone(mergeCommand); if (result.getMergeStatus().isSuccessful()) { - Optional revCommit = doCommit(); + RevCommit revCommit = doCommit().orElseThrow(() -> new NoChangesMadeException(getRepository())); push(); return MergeCommandResult.success(extractRevisionFromRevCommit(revCommit)); } else { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java index 6db9dea066..5055024151 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRevisionExtractor.java @@ -6,10 +6,7 @@ import java.util.Optional; public class GitRevisionExtractor { - static String extractRevisionFromRevCommit(Optional revCommit) { - if (revCommit.isPresent()) { - return revCommit.get().toString().split(" ")[1]; - } - return ""; + static String extractRevisionFromRevCommit(RevCommit revCommit) { + return revCommit.toString().split(" ")[1]; } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index fcd721c3a2..7ed9c1cb75 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -12,6 +12,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Rule; import org.junit.Test; +import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.repository.Person; import sonia.scm.repository.api.MergeCommandResult; @@ -106,7 +107,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { assertThat(mergeCommit.getParent(1).name()).isEqualTo("d81ad6c63d7e2162308d69637b339dedd1d9201c"); } - @Test + @Test(expected = NoChangesMadeException.class) public void shouldNotMergeTwice() throws IOException, GitAPIException { GitMergeCommand command = createCommand(); MergeCommandRequest request = new MergeCommandRequest(); @@ -120,15 +121,9 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { assertThat(mergeCommandResult.isSuccess()).isTrue(); Repository repository = createContext().open(); - ObjectId firstMergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId(); + new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId(); - MergeCommandResult secondMergeCommandResult = command.merge(request); - - assertThat(secondMergeCommandResult.isSuccess()).isTrue(); - - ObjectId secondMergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId(); - - assertThat(secondMergeCommit).isEqualTo(firstMergeCommit); + command.merge(request); } @Test diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java index 514d777f2a..52a609b6cd 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRevisionExtractorTest.java @@ -14,17 +14,8 @@ public class GitRevisionExtractorTest { @Test void shouldReturnRevisionFromRevCommit() { RevCommit revCommit = mock(RevCommit.class); - Optional optionalRevCommit = Optional.of(revCommit); when(revCommit.toString()).thenReturn("commit 123456abcdef -t 4561"); - String revision = GitRevisionExtractor.extractRevisionFromRevCommit(optionalRevCommit); + String revision = GitRevisionExtractor.extractRevisionFromRevCommit(revCommit); assertThat(revision).isEqualTo("123456abcdef"); } - - @Test - void shouldReturnEmptyStringIfRevCommitNotAvailable() { - Optional optionalRevCommit = Optional.empty(); - String revision = GitRevisionExtractor.extractRevisionFromRevCommit(optionalRevCommit); - assertThat(revision).isEqualTo(""); - } - } From 60f5c9a9bfc4c7317c2e7f556a66c2b112e38fa2 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 19 Dec 2019 09:08:41 +0000 Subject: [PATCH 46/57] Close branch bugfix/remove_global_styles_radio_button From ded08d6a1dab57c50e5f9f65cf8b8e3df9c05527 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 19 Dec 2019 10:23:16 +0100 Subject: [PATCH 47/57] supress false positive javasecurity:S2083 on CopyOnWrite --- .../src/main/java/sonia/scm/store/CopyOnWrite.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java index 135221ea82..e4334d42b1 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java @@ -8,6 +8,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; +/** + * CopyOnWrite creates a copy of the target file, before it is modified. This should prevent empty or incomplete files + * on errors such as full disk. + * + * javasecurity:S2083: SonarQube thinks that the path (targetFile) is generated from an http header (HttpUtil), but + * this is not true. It looks like a false-positive, so we suppress the warning for now. + */ +@SuppressWarnings("javasecurity:S2083") public final class CopyOnWrite { private static final Logger LOG = LoggerFactory.getLogger(CopyOnWrite.class); From 148095ad4c1c2a5a613468e0c6baffbf5adfb897 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 19 Dec 2019 10:30:02 +0100 Subject: [PATCH 48/57] Add revisions of merged branches before merge --- .../repository/api/MergeCommandResult.java | 32 +++++++++++--- .../spi/GitFastForwardIfPossible.java | 2 +- .../scm/repository/spi/GitMergeCommit.java | 2 +- .../scm/repository/spi/GitMergeStrategy.java | 42 ++++++++++++++----- .../repository/spi/GitMergeWithSquash.java | 4 +- .../repository/spi/GitMergeCommandTest.java | 7 ++++ 6 files changed, 68 insertions(+), 21 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java index 6a8fd8ab0d..1e6d5c6447 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandResult.java @@ -15,18 +15,22 @@ public class MergeCommandResult { private final Collection filesWithConflict; private final String newHeadRevision; + private final String targetRevision; + private final String revisionToMerge; - private MergeCommandResult(Collection filesWithConflict, String newHeadRevision) { + private MergeCommandResult(Collection filesWithConflict, String targetRevision, String revisionToMerge, String newHeadRevision) { this.filesWithConflict = filesWithConflict; + this.targetRevision = targetRevision; + this.revisionToMerge = revisionToMerge; this.newHeadRevision = newHeadRevision; } - public static MergeCommandResult success(String newHeadRevision) { - return new MergeCommandResult(emptyList(), newHeadRevision); + public static MergeCommandResult success(String targetRevision, String revisionToMerge, String newHeadRevision) { + return new MergeCommandResult(emptyList(), targetRevision, revisionToMerge, newHeadRevision); } - public static MergeCommandResult failure(Collection filesWithConflict) { - return new MergeCommandResult(new HashSet<>(filesWithConflict), null); + public static MergeCommandResult failure(String targetRevision, String revisionToMerge, Collection filesWithConflict) { + return new MergeCommandResult(new HashSet<>(filesWithConflict), targetRevision, revisionToMerge, null); } /** @@ -45,7 +49,25 @@ public class MergeCommandResult { return unmodifiableCollection(filesWithConflict); } + /** + * Returns the revision of the new head of the target branch, if the merge was successful ({@link #isSuccess()}) + */ public String getNewHeadRevision() { return newHeadRevision; } + + /** + * Returns the revision of the target branch prior to the merge. + */ + public String getTargetRevision() { + return targetRevision; + } + + /** + * Returns the revision of the branch that was merged into the target (or in case of a conflict of the revision that + * should have been merged). + */ + public String getRevisionToMerge() { + return revisionToMerge; + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java index 85ba8c2f71..84ea1a0bbc 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java @@ -23,7 +23,7 @@ class GitFastForwardIfPossible extends GitMergeStrategy { MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); if (fastForwardResult.getMergeStatus().isSuccessful()) { push(); - return MergeCommandResult.success(fastForwardResult.getNewHead().toString()); + return createSuccessResult(fastForwardResult.getNewHead().name()); } else { return fallbackMerge.run(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java index 12bbe5f5b1..9a7d290d3a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -29,7 +29,7 @@ class GitMergeCommit extends GitMergeStrategy { if (result.getMergeStatus().isSuccessful()) { RevCommit revCommit = doCommit().orElseThrow(() -> new NoChangesMadeException(getRepository())); push(); - return MergeCommandResult.success(extractRevisionFromRevCommit(revCommit)); + return createSuccessResult(extractRevisionFromRevCommit(revCommit)); } else { return analyseFailure(result); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java index 17b4571820..8e9f79d1b9 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -26,37 +26,57 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker doCommit() { - logger.debug("merged branch {} into {}", toMerge, target); - return doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author); + logger.debug("merged branch {} into {}", branchToMerge, targetBranch); + return doCommit(MessageFormat.format(determineMessageTemplate(), branchToMerge, targetBranch), author); + } + + MergeCommandResult createSuccessResult(String newRevision) { + return MergeCommandResult.success(targetRevision.name(), revisionToMerge.name(), newRevision); + } + + ObjectId getTargetRevision() { + return targetRevision; + } + + ObjectId getRevisionToMerge() { + return revisionToMerge; } private String determineMessageTemplate() { @@ -68,7 +88,7 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker new NoChangesMadeException(getRepository())); push(); - return MergeCommandResult.success(extractRevisionFromRevCommit(revCommit)); + return MergeCommandResult.success(getTargetRevision().name(), revCommit.name(), extractRevisionFromRevCommit(revCommit)); } else { return analyseFailure(result); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index 7ed9c1cb75..2616d5b6e5 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -71,6 +71,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { MergeCommandResult mergeCommandResult = command.merge(request); assertThat(mergeCommandResult.isSuccess()).isTrue(); + assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo("91b99de908fcd04772798a31c308a64aea1a5523"); + assertThat(mergeCommandResult.getTargetRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); Repository repository = createContext().open(); Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); @@ -229,6 +231,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { Repository repository = createContext().open(); assertThat(mergeCommandResult.isSuccess()).isTrue(); + assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo(mergeCommandResult.getNewHeadRevision()); + assertThat(mergeCommandResult.getTargetRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); RevCommit mergeCommit = commits.iterator().next(); @@ -279,6 +283,9 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); MergeCommandResult mergeCommandResult = command.merge(request); + assertThat(mergeCommandResult.getNewHeadRevision()).isEqualTo("35597e9e98fe53167266583848bfef985c2adb27"); + assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo("35597e9e98fe53167266583848bfef985c2adb27"); + assertThat(mergeCommandResult.getTargetRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); assertThat(mergeCommandResult.isSuccess()).isTrue(); From be6c3879bb1839bd912d2226e188b87dda103780 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 19 Dec 2019 10:30:21 +0100 Subject: [PATCH 49/57] update storyshots --- .../src/__snapshots__/storyshots.test.ts.snap | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 25e78abbd8..7fa1b1193f 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Forms|Checkbox Default 1`] = `