diff --git a/scm-core/src/main/java/sonia/scm/repository/FileObject.java b/scm-core/src/main/java/sonia/scm/repository/FileObject.java
index 7dedebb13a..8f1cf298de 100644
--- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java
+++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java
@@ -46,8 +46,11 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Optional;
+import java.util.OptionalLong;
import static java.util.Collections.unmodifiableCollection;
+import static java.util.Optional.ofNullable;
/**
* The FileObject represents a file or a directory in a repository.
@@ -90,7 +93,9 @@ public class FileObject implements LastModifiedAware, Serializable
&& Objects.equal(description, other.description)
&& Objects.equal(length, other.length)
&& Objects.equal(subRepository, other.subRepository)
- && Objects.equal(lastModified, other.lastModified);
+ && Objects.equal(commitDate, other.commitDate)
+ && Objects.equal(partialResult, other.partialResult)
+ && Objects.equal(computationAborted, other.computationAborted);
//J+
}
@@ -100,8 +105,16 @@ public class FileObject implements LastModifiedAware, Serializable
@Override
public int hashCode()
{
- return Objects.hashCode(name, path, directory, description, length,
- subRepository, lastModified);
+ return Objects.hashCode(
+ name,
+ path,
+ directory,
+ description,
+ length,
+ subRepository,
+ commitDate,
+ partialResult,
+ computationAborted);
}
/**
@@ -118,7 +131,9 @@ public class FileObject implements LastModifiedAware, Serializable
.add("description", description)
.add("length", length)
.add("subRepository", subRepository)
- .add("lastModified", lastModified)
+ .add("commitDate", commitDate)
+ .add("partialResult", partialResult)
+ .add("computationAborted", computationAborted)
.toString();
//J+
}
@@ -130,35 +145,44 @@ public class FileObject implements LastModifiedAware, Serializable
* if the repository provider is not able to get the last commit for the path.
*
*
- * @return last commit message
+ * @return Last commit message or null, when this value has not been computed
+ * (see {@link #isPartialResult()}).
*/
- public String getDescription()
+ public Optional getDescription()
{
- return description;
+ return ofNullable(description);
}
/**
* Returns the last commit date for this. The method will return null,
- * if the repository provider is not able to get the last commit for the path.
+ * if the repository provider is not able to get the last commit for the path
+ * or it has not been computed.
*
*
* @return last commit date
*/
@Override
- public Long getLastModified()
- {
- return lastModified;
+ public Long getLastModified() {
+ return this.isPartialResult()? null: this.commitDate;
}
/**
- * Returns the length of the file.
- *
- *
- * @return length of file
+ * Returns the last commit date for this. The method will return {@link OptionalLong#empty()},
+ * if the repository provider is not able to get the last commit for the path or if this value has not been computed
+ * (see {@link #isPartialResult()} and {@link #isComputationAborted()}).
*/
- public long getLength()
+ public OptionalLong getCommitDate()
{
- return length;
+ return commitDate == null? OptionalLong.empty(): OptionalLong.of(commitDate);
+ }
+
+ /**
+ * Returns the length of the file or {@link OptionalLong#empty()}, when this value has not been computed
+ * (see {@link #isPartialResult()} and {@link #isComputationAborted()}).
+ */
+ public OptionalLong getLength()
+ {
+ return length == null? OptionalLong.empty(): OptionalLong.of(length);
}
/**
@@ -200,7 +224,7 @@ public class FileObject implements LastModifiedAware, Serializable
}
/**
- * Return sub repository informations or null if the file is not
+ * Return sub repository information or null if the file is not
* sub repository.
*
* @since 1.10
@@ -222,6 +246,42 @@ public class FileObject implements LastModifiedAware, Serializable
return directory;
}
+ /**
+ * Returns the children of this file.
+ *
+ * @return The children of this file if it is a directory.
+ */
+ public Collection getChildren() {
+ return children == null? null: unmodifiableCollection(children);
+ }
+
+ /**
+ * If this is true, some values for this object have not been computed, yet. These values (like
+ * {@link #getLength()}, {@link #getDescription()} or {@link #getCommitDate()})
+ * will return {@link Optional#empty()} (or {@link OptionalLong#empty()} respectively), unless they are computed.
+ * There may be an asynchronous task running, that will set these values in the future.
+ *
+ * @since 2.0.0
+ *
+ * @return true, whenever some values of this object have not been computed, yet.
+ */
+ public boolean isPartialResult() {
+ return partialResult;
+ }
+
+ /**
+ * If this is true, some values for this object have not been computed and will not be computed. These
+ * values (like {@link #getLength()}, {@link #getDescription()} or {@link #getCommitDate()})
+ * will return {@link Optional#empty()} (or {@link OptionalLong#empty()} respectively), unless they are computed.
+ *
+ * @since 2.0.0
+ *
+ * @return true, whenever some values of this object finally are not computed.
+ */
+ public boolean isComputationAborted() {
+ return computationAborted;
+ }
+
//~--- set methods ----------------------------------------------------------
/**
@@ -247,14 +307,14 @@ public class FileObject implements LastModifiedAware, Serializable
}
/**
- * Sets the last modified date of the file.
+ * Sets the commit date of the file.
*
*
- * @param lastModified last modified date
+ * @param commitDate commit date
*/
- public void setLastModified(Long lastModified)
+ public void setCommitDate(Long commitDate)
{
- this.lastModified = lastModified;
+ this.commitDate = commitDate;
}
/**
@@ -263,7 +323,7 @@ public class FileObject implements LastModifiedAware, Serializable
*
* @param length file length
*/
- public void setLength(long length)
+ public void setLength(Long length)
{
this.length = length;
}
@@ -302,22 +362,47 @@ public class FileObject implements LastModifiedAware, Serializable
this.subRepository = subRepository;
}
- public Collection getChildren() {
- return unmodifiableCollection(children);
+ /**
+ * Set marker, that some values for this object are not computed, yet.
+ *
+ * @since 2.0.0
+ *
+ * @param partialResult Set this to true, whenever some values of this object are not computed, yet.
+ */
+ public void setPartialResult(boolean partialResult) {
+ this.partialResult = partialResult;
}
+ /**
+ * Set marker, that computation of some values for this object has been aborted.
+ *
+ * @since 2.0.0
+ *
+ * @param computationAborted Set this to true, whenever some values of this object are not computed and
+ * will not be computed in the future.
+ */
+ public void setComputationAborted(boolean computationAborted) {
+ this.computationAborted = computationAborted;
+ }
+
+ /**
+ * Set the children for this file.
+ *
+ * @param children The new childre.
+ */
public void setChildren(List children) {
this.children = new ArrayList<>(children);
}
+ /**
+ * Adds a child to the list of children .
+ *
+ * @param child The additional child.
+ */
public void addChild(FileObject child) {
this.children.add(child);
}
- public boolean hasChildren() {
- return !children.isEmpty();
- }
-
//~--- fields ---------------------------------------------------------------
/** file description */
@@ -326,11 +411,11 @@ public class FileObject implements LastModifiedAware, Serializable
/** directory indicator */
private boolean directory;
- /** last modified date */
- private Long lastModified;
+ /** commit date */
+ private Long commitDate;
/** file length */
- private long length;
+ private Long length;
/** filename */
private String name;
@@ -338,9 +423,16 @@ public class FileObject implements LastModifiedAware, Serializable
/** file path */
private String path;
+ /** Marker for partial result. */
+ private boolean partialResult = false;
+
+ /** Marker for aborted computation. */
+ private boolean computationAborted = false;
+
/** sub repository informations */
@XmlElement(name = "subrepository")
private SubRepository subRepository;
+ /** Children of this file (aka directory). */
private Collection children = new ArrayList<>();
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
index d482c04ea4..563557f0c1 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
@@ -199,7 +199,7 @@ public final class BrowseCommandBuilder
return this;
}
-
+
/**
* Disabling the last commit means that every call to
* {@link FileObject#getDescription()} and
@@ -300,6 +300,13 @@ public final class BrowseCommandBuilder
return this;
}
+ private void updateCache(BrowserResult updatedResult) {
+ if (!disableCache) {
+ CacheKey key = new CacheKey(repository, request);
+ cache.put(key, updatedResult);
+ }
+ }
+
//~--- inner classes --------------------------------------------------------
/**
@@ -416,5 +423,5 @@ public final class BrowseCommandBuilder
private final Repository repository;
/** request for the command */
- private final BrowseCommandRequest request = new BrowseCommandRequest();
+ private final BrowseCommandRequest request = new BrowseCommandRequest(this::updateCache);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java
index 39da9a9ace..9c23fe93f2 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java
@@ -37,6 +37,10 @@ package sonia.scm.repository.spi;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
+import sonia.scm.repository.BrowserResult;
+
+import java.util.function.Consumer;
+
/**
*
* @author Sebastian Sdorra
@@ -48,6 +52,14 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest
/** Field description */
private static final long serialVersionUID = 7956624623516803183L;
+ public BrowseCommandRequest() {
+ this(null);
+ }
+
+ public BrowseCommandRequest(Consumer updater) {
+ this.updater = updater;
+ }
+
//~--- methods --------------------------------------------------------------
/**
@@ -220,6 +232,12 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest
return recursive;
}
+ public void updateCache(BrowserResult update) {
+ if (updater != null) {
+ updater.accept(update);
+ }
+ }
+
//~--- fields ---------------------------------------------------------------
/** disable last commit */
@@ -230,4 +248,8 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest
/** browse file objects recursive */
private boolean recursive = false;
+
+ // WARNING / TODO: This field creates a reverse channel from the implementation to the API. This will break
+ // whenever the API runs in a different process than the SPI (for example to run explicit hosts for git repositories).
+ private final transient Consumer updater;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java
new file mode 100644
index 0000000000..a9e7d85dcc
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java
@@ -0,0 +1,83 @@
+package sonia.scm.repository.spi;
+
+import java.util.function.Consumer;
+
+/**
+ * Tasks submitted to this executor will be run synchronously up to a given time, after which they will be queued and
+ * processed asynchronously. After a maximum amount of time consumed by these tasks, they will be skipped. Note that
+ * this only works for short-living tasks.
+ *
+ * Get instances of this using a {@link SyncAsyncExecutorProvider}.
+ */
+public interface SyncAsyncExecutor {
+
+ /**
+ * Execute the given task (either synchronously or asynchronously). If this task is skipped due to
+ * timeouts, nothing will be done.
+ *
+ * @param task The {@link Runnable} to be executed.
+ * @return Either {@link ExecutionType#SYNCHRONOUS} when the given {@link Runnable} has been executed immediately or
+ * {@link ExecutionType#ASYNCHRONOUS}, when the task was queued to be executed asynchronously in the future.
+ */
+ default ExecutionType execute(Runnable task) {
+ return execute(
+ ignored -> task.run(),
+ () -> {}
+ );
+ }
+
+ /**
+ * Execute the given task (either synchronously or asynchronously). If this task is
+ * skipped due to timeouts, the abortionFallback will be called.
+ *
+ * @param task The {@link Runnable} to be executed.
+ * @param abortionFallback This will only be run, when this and all remaining tasks are aborted. This task should
+ * only consume a negligible amount of time.
+ * @return Either {@link ExecutionType#SYNCHRONOUS} when the given {@link Runnable} has been executed immediately or
+ * {@link ExecutionType#ASYNCHRONOUS}, when the task was queued to be executed asynchronously in the future.
+ */
+ default ExecutionType execute(Runnable task, Runnable abortionFallback) {
+ return execute(ignored -> task.run(), abortionFallback);
+ }
+
+ /**
+ * Execute the given task (either synchronously or asynchronously). If this task is skipped due to
+ * timeouts, nothing will be done.
+ *
+ * @param task The {@link Consumer} to be executed. The parameter given to this is either
+ * {@link ExecutionType#SYNCHRONOUS} when the given {@link Consumer} is executed immediately
+ * or {@link ExecutionType#ASYNCHRONOUS}, when the task had been queued and now is executed
+ * asynchronously.
+ * @return Either {@link ExecutionType#SYNCHRONOUS} when the given {@link Runnable} has been executed immediately or
+ * {@link ExecutionType#ASYNCHRONOUS}, when the task was queued to be executed asynchronously in the future.
+ */
+ default ExecutionType execute(Consumer task) {
+ return execute(task, () -> {});
+ }
+
+ /**
+ * Execute the given task (either synchronously or asynchronously). If this task is
+ * skipped due to timeouts, the abortionFallback will be called.
+ *
+ * @param task The {@link Consumer} to be executed. The parameter given to this is either
+ * {@link ExecutionType#SYNCHRONOUS} when the given {@link Consumer} is executed immediately
+ * or {@link ExecutionType#ASYNCHRONOUS}, when the task had been queued and now is executed
+ * asynchronously.
+ * @param abortionFallback This will only be run, when this and all remaining tasks are aborted. This task should
+ * only consume a negligible amount of time.
+ * @return Either {@link ExecutionType#SYNCHRONOUS} when the given {@link Runnable} has been executed immediately or
+ * {@link ExecutionType#ASYNCHRONOUS}, when the task was queued to be executed asynchronously in the future.
+ */
+ ExecutionType execute(Consumer task, Runnable abortionFallback);
+
+ /**
+ * When all submitted tasks have been executed synchronously, this will return true. If at least one task
+ * has been enqueued to be executed asynchronously, this returns false (even when none of the enqueued
+ * tasks have been run, yet).
+ */
+ boolean hasExecutedAllSynchronously();
+
+ enum ExecutionType {
+ SYNCHRONOUS, ASYNCHRONOUS
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java
new file mode 100644
index 0000000000..5f417f324e
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java
@@ -0,0 +1,56 @@
+package sonia.scm.repository.spi;
+
+/**
+ * Use this provider to get {@link SyncAsyncExecutor} instances to execute a number of normally short-lived tasks, that
+ * should be run asynchronously (or even be skipped) whenever they take too long in summary.
+ *
+ * The goal of this is a "best effort" approach: The submitted tasks are run immediately when they are submitted, unless
+ * a given timespan (switchToAsyncInSeconds) has passed. From this moment on the tasks are put into a queue to be
+ * processed asynchronously. If even then they take too long and their accumulated asynchronous runtime exceeds another
+ * limit (maxAsyncAbortSeconds), the tasks are skipped.
+ *
+ * Note that whenever a task has been started either synchronously or asynchronously it will neither be terminated nor
+ * switched from foreground to background execution, so this will only work well for short-living tasks. A long running
+ * task can still block this for longer than the configured amount of seconds.
+ */
+public interface SyncAsyncExecutorProvider {
+
+ int DEFAULT_SWITCH_TO_ASYNC_IN_SECONDS = 2;
+
+ /**
+ * Creates an {@link SyncAsyncExecutor} that will run tasks synchronously for
+ * {@link #DEFAULT_SWITCH_TO_ASYNC_IN_SECONDS} seconds. The limit of asynchronous runtime is implementation dependant.
+ *
+ * @return The executor.
+ */
+ default SyncAsyncExecutor createExecutorWithDefaultTimeout() {
+ return createExecutorWithSecondsToTimeout(DEFAULT_SWITCH_TO_ASYNC_IN_SECONDS);
+ }
+
+ /**
+ * Creates an {@link SyncAsyncExecutor} that will run tasks synchronously for
+ * switchToAsyncInSeconds seconds. The limit of asynchronous runtime is implementation dependant.
+ *
+ * @param switchToAsyncInSeconds The amount of seconds submitted tasks will be run synchronously. After this time,
+ * further tasks will be run asynchronously. To run all tasks asynchronously no matter
+ * what, set this to 0.
+ * @return The executor.
+ */
+ SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds);
+
+ /**
+ * Creates an {@link SyncAsyncExecutor} that will run tasks synchronously for
+ * switchToAsyncInSeconds seconds and will abort tasks after they ran
+ * maxAsyncAbortSeconds asynchronously.
+ *
+ * @param switchToAsyncInSeconds The amount of seconds submitted tasks will be run synchronously. After this time,
+ * further tasks will be run asynchronously. To run all tasks asynchronously no matter
+ * what, set this to 0.
+ * @param maxAsyncAbortSeconds The amount of seconds, tasks that were started asynchronously may run in summary
+ * before remaining tasks will not be executed at all anymore. To abort all tasks that
+ * are submitted after switchToAsyncInSeconds immediately, set this to
+ * 0.
+ * @return The executor.
+ */
+ SyncAsyncExecutor createExecutorWithSecondsToTimeout(int switchToAsyncInSeconds, int maxAsyncAbortSeconds);
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java
index d726b992ca..a93c1b5d81 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java
@@ -745,6 +745,10 @@ public final class GitUtil
public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) throws IOException {
Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit);
+ return getLfsPointer(repo, treeWalk, attributes);
+ }
+
+ public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, Attributes attributes) throws IOException {
Attribute filter = attributes.get("filter");
if (filter != null && "lfs".equals(filter.getValue())) {
ObjectId blobId = treeWalk.getObjectId(0);
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
index aa362b8ec6..e8ef5a7a33 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
@@ -35,9 +35,11 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
+import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import org.eclipse.jgit.attributes.Attributes;
import org.eclipse.jgit.lfs.LfsPointer;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -49,6 +51,7 @@ import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.LfsFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
@@ -56,6 +59,7 @@ import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitSubModuleParser;
import sonia.scm.repository.GitUtil;
+import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.SubRepository;
import sonia.scm.store.Blob;
@@ -69,10 +73,13 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.function.Consumer;
import static java.util.Optional.empty;
+import static java.util.Optional.of;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
+import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS;
//~--- JDK imports ------------------------------------------------------------
@@ -90,71 +97,56 @@ public class GitBrowseCommand extends AbstractGitCommand
/**
* the logger for GitBrowseCommand
*/
- private static final Logger logger =
- LoggerFactory.getLogger(GitBrowseCommand.class);
+ private static final Logger logger = LoggerFactory.getLogger(GitBrowseCommand.class);
+
+ /** sub repository cache */
+ private final Map> subrepositoryCache = Maps.newHashMap();
+
+ private final Object asyncMonitor = new Object();
+
private final LfsBlobStoreFactory lfsBlobStoreFactory;
- //~--- constructors ---------------------------------------------------------
+ private final SyncAsyncExecutor executor;
- /**
- * Constructs ...
- * @param context
- * @param repository
- * @param lfsBlobStoreFactory
- */
- public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory)
- {
+ private BrowserResult browserResult;
+
+ public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) {
super(context, repository);
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
+ this.executor = executor;
}
- //~--- get methods ----------------------------------------------------------
-
@Override
- @SuppressWarnings("unchecked")
public BrowserResult getBrowserResult(BrowseCommandRequest request)
throws IOException {
logger.debug("try to create browse result for {}", request);
- BrowserResult result;
-
org.eclipse.jgit.lib.Repository repo = open();
- ObjectId revId;
+ ObjectId revId = computeRevIdToBrowse(request, repo);
- if (Util.isEmpty(request.getRevision()))
- {
- revId = getDefaultBranch(repo);
- }
- else
- {
- revId = GitUtil.getRevisionId(repo, request.getRevision());
+ if (revId != null) {
+ browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId));
+ return browserResult;
+ } else {
+ logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName());
+ return new BrowserResult(Constants.HEAD, request.getRevision(), createEmptyRoot());
}
+ }
- if (revId != null)
- {
- result = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId));
- }
- else
- {
- if (Util.isNotEmpty(request.getRevision()))
- {
+ private ObjectId computeRevIdToBrowse(BrowseCommandRequest request, org.eclipse.jgit.lib.Repository repo) throws IOException {
+ if (Util.isEmpty(request.getRevision())) {
+ return getDefaultBranch(repo);
+ } else {
+ ObjectId revId = GitUtil.getRevisionId(repo, request.getRevision());
+ if (revId == null) {
logger.error("could not find revision {}", request.getRevision());
throw notFound(entity("Revision", request.getRevision()).in(this.repository));
}
- else if (logger.isWarnEnabled())
- {
- logger.warn("could not find head of repository, empty?");
- }
-
- result = new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot());
+ return revId;
}
-
- return result;
}
- //~--- methods --------------------------------------------------------------
-
- private FileObject createEmtpyRoot() {
+ private FileObject createEmptyRoot() {
FileObject fileObject = new FileObject();
fileObject.setName("");
fileObject.setPath("");
@@ -162,18 +154,6 @@ public class GitBrowseCommand extends AbstractGitCommand
return fileObject;
}
- /**
- * Method description
- *
- * @param repo
- * @param request
- * @param revId
- * @param treeWalk
- *
- * @return
- *
- * @throws IOException
- */
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
throws IOException {
@@ -207,127 +187,62 @@ public class GitBrowseCommand extends AbstractGitCommand
// don't show message and date for directories to improve performance
if (!file.isDirectory() &&!request.isDisableLastCommit())
{
- logger.trace("fetch last commit for {} at {}", path, revId.getName());
- RevCommit commit = getLatestCommit(repo, revId, path);
-
- Optional lfsPointer = commit == null? empty(): GitUtil.getLfsPointer(repo, path, commit, treeWalk);
+ file.setPartialResult(true);
+ RevCommit commit;
+ try (RevWalk walk = new RevWalk(repo)) {
+ commit = walk.parseCommit(revId);
+ }
+ Optional lfsPointer = getLfsPointer(repo, path, commit, treeWalk);
if (lfsPointer.isPresent()) {
- BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
- String oid = lfsPointer.get().getOid().getName();
- Blob blob = lfsBlobStore.get(oid);
- if (blob == null) {
- logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName());
- file.setLength(-1);
- } else {
- file.setLength(blob.getSize());
- }
+ setFileLengthFromLfsBlob(lfsPointer.get(), file);
} else {
file.setLength(loader.getSize());
}
- if (commit != null)
- {
- file.setLastModified(GitUtil.getCommitTime(commit));
- file.setDescription(commit.getShortMessage());
- }
- else if (logger.isWarnEnabled())
- {
- logger.warn("could not find latest commit for {} on {}", path,
- revId);
- }
+ executor.execute(
+ new CompleteFileInformation(path, revId, repo, file, request),
+ new AbortFileInformation(request)
+ );
}
}
return file;
}
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- *
- * @param repo
- * @param revId
- * @param path
- *
- * @return
- */
- private RevCommit getLatestCommit(org.eclipse.jgit.lib.Repository repo,
- ObjectId revId, String path)
- {
- RevCommit result = null;
- RevWalk walk = null;
-
- try
- {
- walk = new RevWalk(repo);
- walk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path),
- TreeFilter.ANY_DIFF));
-
- RevCommit commit = walk.parseCommit(revId);
-
- walk.markStart(commit);
- result = Util.getFirst(walk);
- }
- catch (IOException ex)
- {
- logger.error("could not parse commit for file", ex);
- }
- finally
- {
- GitUtil.release(walk);
- }
-
- return result;
+ private void updateCache(BrowseCommandRequest request) {
+ request.updateCache(browserResult);
+ logger.info("updated browser result for repository {}", repository.getNamespaceAndName());
}
private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException {
- RevWalk revWalk = null;
- TreeWalk treeWalk = null;
-
- FileObject result;
-
- try {
+ try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo)) {
logger.debug("load repository browser for revision {}", revId.name());
- treeWalk = new TreeWalk(repo);
if (!isRootRequest(request)) {
treeWalk.setFilter(PathFilter.create(request.getPath()));
}
- revWalk = new RevWalk(repo);
RevTree tree = revWalk.parseTree(revId);
- if (tree != null)
- {
+ if (tree != null) {
treeWalk.addTree(tree);
- }
- else
- {
+ } else {
throw new IllegalStateException("could not find tree for " + revId.name());
}
if (isRootRequest(request)) {
- result = createEmtpyRoot();
+ FileObject result = createEmptyRoot();
findChildren(result, repo, request, revId, treeWalk);
+ return result;
} else {
- result = findFirstMatch(repo, request, revId, treeWalk);
+ FileObject result = findFirstMatch(repo, request, revId, treeWalk);
if ( result.isDirectory() ) {
treeWalk.enterSubtree();
findChildren(result, repo, request, revId, treeWalk);
}
+ return result;
}
-
}
- finally
- {
- GitUtil.release(revWalk);
- GitUtil.release(treeWalk);
- }
-
- return result;
}
private boolean isRootRequest(BrowseCommandRequest request) {
@@ -384,56 +299,144 @@ public class GitBrowseCommand extends AbstractGitCommand
throw notFound(entity("File", request.getPath()).in("Revision", revId.getName()).in(this.repository));
}
- @SuppressWarnings("unchecked")
- private Map getSubRepositories(org.eclipse.jgit.lib.Repository repo,
- ObjectId revision)
+ private Map getSubRepositories(org.eclipse.jgit.lib.Repository repo, ObjectId revision)
throws IOException {
- if (logger.isDebugEnabled())
- {
- logger.debug("read submodules of {} at {}", repository.getName(),
- revision);
- }
- Map subRepositories;
- try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() )
- {
+ logger.debug("read submodules of {} at {}", repository.getName(), revision);
+
+ try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) {
new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision,
PATH_MODULES, baos);
- subRepositories = GitSubModuleParser.parse(baos.toString());
+ return GitSubModuleParser.parse(baos.toString());
+ } catch (NotFoundException ex) {
+ logger.trace("could not find .gitmodules: {}", ex.getMessage());
+ return Collections.emptyMap();
}
- catch (NotFoundException ex)
- {
- logger.trace("could not find .gitmodules", ex);
- subRepositories = Collections.EMPTY_MAP;
- }
-
- return subRepositories;
}
- private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo,
- ObjectId revId, String path)
+ private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path)
throws IOException {
Map subRepositories = subrepositoryCache.get(revId);
- if (subRepositories == null)
- {
+ if (subRepositories == null) {
subRepositories = getSubRepositories(repo, revId);
subrepositoryCache.put(revId, subRepositories);
}
- SubRepository sub = null;
-
- if (subRepositories != null)
- {
- sub = subRepositories.get(path);
+ if (subRepositories != null) {
+ return subRepositories.get(path);
}
-
- return sub;
+ return null;
}
- //~--- fields ---------------------------------------------------------------
+ private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) {
+ try {
+ Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit);
- /** sub repository cache */
- private final Map> subrepositoryCache = Maps.newHashMap();
+ return GitUtil.getLfsPointer(repo, treeWalk, attributes);
+ } catch (IOException e) {
+ throw new InternalRepositoryException(repository, "could not read lfs pointer", e);
+ }
+ }
+
+ private void setFileLengthFromLfsBlob(LfsPointer lfsPointer, FileObject file) {
+ BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
+ String oid = lfsPointer.getOid().getName();
+ Blob blob = lfsBlobStore.get(oid);
+ if (blob == null) {
+ logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName());
+ file.setLength(null);
+ } else {
+ file.setLength(blob.getSize());
+ }
+ }
+
+ private class CompleteFileInformation implements Consumer {
+ private final String path;
+ private final ObjectId revId;
+ private final org.eclipse.jgit.lib.Repository repo;
+ private final FileObject file;
+ private final BrowseCommandRequest request;
+
+ public CompleteFileInformation(String path, ObjectId revId, org.eclipse.jgit.lib.Repository repo, FileObject file, BrowseCommandRequest request) {
+ this.path = path;
+ this.revId = revId;
+ this.repo = repo;
+ this.file = file;
+ this.request = request;
+ }
+
+ @Override
+ public void accept(SyncAsyncExecutor.ExecutionType executionType) {
+ logger.trace("fetch last commit for {} at {}", path, revId.getName());
+
+ Stopwatch sw = Stopwatch.createStarted();
+
+ Optional commit = getLatestCommit(repo, revId, path);
+
+ synchronized (asyncMonitor) {
+ file.setPartialResult(false);
+ if (commit.isPresent()) {
+ applyValuesFromCommit(executionType, commit.get());
+ } else {
+ logger.warn("could not find latest commit for {} on {}", path, revId);
+ }
+ }
+
+ logger.trace("finished loading of last commit {} of {} in {}", revId.getName(), path, sw.stop());
+ }
+
+ private Optional getLatestCommit(org.eclipse.jgit.lib.Repository repo,
+ ObjectId revId, String path) {
+ try (RevWalk walk = new RevWalk(repo)) {
+ walk.setTreeFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, PathFilter.create(path)));
+
+ RevCommit commit = walk.parseCommit(revId);
+
+ walk.markStart(commit);
+ return of(Util.getFirst(walk));
+ } catch (IOException ex) {
+ logger.error("could not parse commit for file", ex);
+ return empty();
+ }
+ }
+
+ private void applyValuesFromCommit(SyncAsyncExecutor.ExecutionType executionType, RevCommit commit) {
+ file.setCommitDate(GitUtil.getCommitTime(commit));
+ file.setDescription(commit.getShortMessage());
+ if (executionType == ASYNCHRONOUS && browserResult != null) {
+ updateCache(request);
+ }
+ }
+ }
+
+ private class AbortFileInformation implements Runnable {
+ private final BrowseCommandRequest request;
+
+ public AbortFileInformation(BrowseCommandRequest request) {
+ this.request = request;
+ }
+
+ @Override
+ public void run() {
+ synchronized (asyncMonitor) {
+ if (markPartialAsAborted(browserResult.getFile())) {
+ updateCache(request);
+ }
+ }
+ }
+
+ private boolean markPartialAsAborted(FileObject file) {
+ boolean changed = false;
+ if (file.isPartialResult()) {
+ file.setPartialResult(false);
+ file.setComputationAborted(true);
+ changed = true;
+ }
+ for (FileObject child : file.getChildren()) {
+ changed |= markPartialAsAborted(child);
+ }
+ return changed;
+ }
+ }
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
index e1ae58ada5..fa54ca6007 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java
@@ -80,12 +80,13 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
//~--- constructors ---------------------------------------------------------
- public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
+ public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) {
this.handler = handler;
this.repository = repository;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
+ this.executorProvider = executorProvider;
this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
}
@@ -150,7 +151,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
@Override
public BrowseCommand getBrowseCommand()
{
- return new GitBrowseCommand(context, repository, lfsBlobStoreFactory);
+ return new GitBrowseCommand(context, repository, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout());
}
/**
@@ -301,4 +302,6 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
+
+ private final SyncAsyncExecutorProvider executorProvider;
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java
index 7fc5fb27c4..1bb7e84b92 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java
@@ -55,14 +55,16 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
private final LfsBlobStoreFactory lfsBlobStoreFactory;
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
+ private final SyncAsyncExecutorProvider executorProvider;
@Inject
- public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
+ public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) {
this.handler = handler;
this.storeProvider = storeProvider;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
+ this.executorProvider = executorProvider;
}
@Override
@@ -70,7 +72,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
GitRepositoryServiceProvider provider = null;
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
- provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus);
+ provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus, executorProvider);
}
return provider;
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
index 4b854f6209..39066f0a9d 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
@@ -35,15 +35,21 @@ import org.junit.Test;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitRepositoryConfig;
+import sonia.scm.repository.spi.SyncAsyncExecutors.AsyncExecutorStepper;
import java.io.IOException;
import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static sonia.scm.repository.spi.SyncAsyncExecutors.stepperAsynchronousExecutor;
+import static sonia.scm.repository.spi.SyncAsyncExecutors.synchronousExecutor;
/**
* Unit tests for {@link GitBrowseCommand}.
@@ -102,15 +108,55 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
assertFalse(a.isDirectory());
assertEquals("a.txt", a.getName());
assertEquals("a.txt", a.getPath());
- assertEquals("added new line for blame", a.getDescription());
- assertTrue(a.getLength() > 0);
- checkDate(a.getLastModified());
+ assertEquals("added new line for blame", a.getDescription().get());
+ assertTrue(a.getLength().getAsLong() > 0);
+ checkDate(a.getCommitDate().getAsLong());
assertTrue(c.isDirectory());
assertEquals("c", c.getName());
assertEquals("c", c.getPath());
}
+ @Test
+ public void testAsynchronousBrowse() throws IOException {
+ try (AsyncExecutorStepper executor = stepperAsynchronousExecutor()) {
+ GitBrowseCommand command = new GitBrowseCommand(createContext(), repository, null, executor);
+ List updatedResults = new LinkedList<>();
+ BrowseCommandRequest request = new BrowseCommandRequest(updatedResults::add);
+ FileObject root = command.getBrowserResult(request).getFile();
+ assertNotNull(root);
+
+ Collection foList = root.getChildren();
+
+ FileObject a = findFile(foList, "a.txt");
+ FileObject b = findFile(foList, "b.txt");
+
+ assertTrue(a.isPartialResult());
+ assertFalse("expected empty name before commit could have been read", a.getDescription().isPresent());
+ assertFalse("expected empty date before commit could have been read", a.getCommitDate().isPresent());
+ assertTrue(b.isPartialResult());
+ assertFalse("expected empty name before commit could have been read", b.getDescription().isPresent());
+ assertFalse("expected empty date before commit could have been read", b.getCommitDate().isPresent());
+
+ executor.next();
+
+ assertEquals(1, updatedResults.size());
+ assertFalse(a.isPartialResult());
+ assertNotNull("expected correct name after commit could have been read", a.getDescription());
+ assertTrue("expected correct date after commit could have been read", a.getCommitDate().isPresent());
+ assertTrue(b.isPartialResult());
+ assertFalse("expected empty name before commit could have been read", b.getDescription().isPresent());
+ assertFalse("expected empty date before commit could have been read", b.getCommitDate().isPresent());
+
+ executor.next();
+
+ assertEquals(2, updatedResults.size());
+ assertFalse(b.isPartialResult());
+ assertNotNull("expected correct name after commit could have been read", b.getDescription());
+ assertTrue("expected correct date after commit could have been read", b.getCommitDate().isPresent());
+ }
+ }
+
@Test
public void testBrowseSubDirectory() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest();
@@ -129,20 +175,20 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
assertFalse(d.isDirectory());
assertEquals("d.txt", d.getName());
assertEquals("c/d.txt", d.getPath());
- assertEquals("added file d and e in folder c", d.getDescription());
- assertTrue(d.getLength() > 0);
- checkDate(d.getLastModified());
+ assertEquals("added file d and e in folder c", d.getDescription().get());
+ assertTrue(d.getLength().getAsLong() > 0);
+ checkDate(d.getCommitDate().getAsLong());
assertFalse(e.isDirectory());
assertEquals("e.txt", e.getName());
assertEquals("c/e.txt", e.getPath());
- assertEquals("added file d and e in folder c", e.getDescription());
- assertTrue(e.getLength() > 0);
- checkDate(e.getLastModified());
+ assertEquals("added file d and e in folder c", e.getDescription().get());
+ assertTrue(e.getLength().getAsLong() > 0);
+ checkDate(e.getCommitDate().getAsLong());
}
@Test
- public void testRecusive() throws IOException {
+ public void testRecursive() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setRecursive(true);
@@ -171,6 +217,6 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
}
private GitBrowseCommand createCommand() {
- return new GitBrowseCommand(createContext(), repository, null);
+ return new GitBrowseCommand(createContext(), repository, null, synchronousExecutor());
}
}
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java
index 0897a191a1..4d5d5e8646 100644
--- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java
@@ -231,13 +231,13 @@ public class HgFileviewCommand extends AbstractCommand
file.setName(getNameFromPath(path));
file.setPath(path);
file.setDirectory(false);
- file.setLength(stream.decimalIntUpTo(' '));
+ file.setLength((long) stream.decimalIntUpTo(' '));
DateTime timestamp = stream.dateTimeUpTo(' ');
String description = stream.textUpTo('\0');
if (!disableLastCommit) {
- file.setLastModified(timestamp.getDate().getTime());
+ file.setCommitDate(timestamp.getDate().getTime());
file.setDescription(description);
}
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java
index 2116d06a7a..92a05a05a0 100644
--- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java
+++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java
@@ -61,7 +61,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
FileObject file = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile();
assertEquals("a.txt", file.getName());
assertFalse(file.isDirectory());
- assertTrue(file.getChildren().isEmpty());
+ assertTrue(file.getChildren() == null || file.getChildren().isEmpty());
}
@Test
@@ -73,9 +73,9 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
assertFalse(a.isDirectory());
assertEquals("a.txt", a.getName());
assertEquals("a.txt", a.getPath());
- assertEquals("added new line for blame", a.getDescription());
- assertTrue(a.getLength() > 0);
- checkDate(a.getLastModified());
+ assertEquals("added new line for blame", a.getDescription().get());
+ assertTrue(a.getLength().getAsLong() > 0);
+ checkDate(a.getCommitDate().getAsLong());
assertTrue(c.isDirectory());
assertEquals("c", c.getName());
assertEquals("c", c.getPath());
@@ -132,16 +132,16 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
assertFalse(d.isDirectory());
assertEquals("d.txt", d.getName());
assertEquals("c/d.txt", d.getPath());
- assertEquals("added file d and e in folder c", d.getDescription());
- assertTrue(d.getLength() > 0);
- checkDate(d.getLastModified());
+ assertEquals("added file d and e in folder c", d.getDescription().get());
+ assertTrue(d.getLength().getAsLong() > 0);
+ checkDate(d.getCommitDate().getAsLong());
assertNotNull(e);
assertFalse(e.isDirectory());
assertEquals("e.txt", e.getName());
assertEquals("c/e.txt", e.getPath());
- assertEquals("added file d and e in folder c", e.getDescription());
- assertTrue(e.getLength() > 0);
- checkDate(e.getLastModified());
+ assertEquals("added file d and e in folder c", e.getDescription().get());
+ assertTrue(e.getLength().getAsLong() > 0);
+ checkDate(e.getCommitDate().getAsLong());
}
@Test
@@ -154,8 +154,8 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
FileObject a = getFileObject(foList, "a.txt");
- assertNull(a.getDescription());
- assertNull(a.getLastModified());
+ assertFalse(a.getDescription().isPresent());
+ assertFalse(a.getCommitDate().isPresent());
}
@Test
diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java
index 99dae0e77b..e4a32c8ca6 100644
--- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java
+++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java
@@ -173,7 +173,7 @@ public class SvnBrowseCommand extends AbstractSvnCommand
{
if (entry.getDate() != null)
{
- fileObject.setLastModified(entry.getDate().getTime());
+ fileObject.setCommitDate(entry.getDate().getTime());
}
fileObject.setDescription(entry.getCommitMessage());
diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java
index d3e6a98558..980d486b5c 100644
--- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java
+++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java
@@ -60,7 +60,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
FileObject file = createCommand().getBrowserResult(request).getFile();
assertEquals("a.txt", file.getName());
assertFalse(file.isDirectory());
- assertTrue(file.getChildren().isEmpty());
+ assertTrue(file.getChildren() == null || file.getChildren().isEmpty());
}
@Test
@@ -73,9 +73,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
assertFalse(a.isDirectory());
assertEquals("a.txt", a.getName());
assertEquals("a.txt", a.getPath());
- assertEquals("added line for blame test", a.getDescription());
- assertTrue(a.getLength() > 0);
- checkDate(a.getLastModified());
+ assertEquals("added line for blame test", a.getDescription().get());
+ assertTrue(a.getLength().getAsLong() > 0);
+ checkDate(a.getCommitDate().getAsLong());
assertTrue(c.isDirectory());
assertEquals("c", c.getName());
assertEquals("c", c.getPath());
@@ -122,16 +122,16 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
assertFalse(d.isDirectory());
assertEquals("d.txt", d.getName());
assertEquals("c/d.txt", d.getPath());
- assertEquals("added d and e in folder c", d.getDescription());
- assertTrue(d.getLength() > 0);
- checkDate(d.getLastModified());
+ assertEquals("added d and e in folder c", d.getDescription().get());
+ assertTrue(d.getLength().getAsLong() > 0);
+ checkDate(d.getCommitDate().getAsLong());
assertNotNull(e);
assertFalse(e.isDirectory());
assertEquals("e.txt", e.getName());
assertEquals("c/e.txt", e.getPath());
- assertEquals("added d and e in folder c", e.getDescription());
- assertTrue(e.getLength() > 0);
- checkDate(e.getLastModified());
+ assertEquals("added d and e in folder c", e.getDescription().get());
+ assertTrue(e.getLength().getAsLong() > 0);
+ checkDate(e.getCommitDate().getAsLong());
}
@Test
@@ -144,8 +144,8 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
FileObject a = getFileObject(foList, "a.txt");
- assertNull(a.getDescription());
- assertNull(a.getLastModified());
+ assertFalse(a.getDescription().isPresent());
+ assertFalse(a.getCommitDate().isPresent());
}
@Test
diff --git a/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java
new file mode 100644
index 0000000000..6c24e72b69
--- /dev/null
+++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java
@@ -0,0 +1,107 @@
+package sonia.scm.repository.spi;
+
+import java.io.Closeable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.function.Consumer;
+
+import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS;
+import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONOUS;
+
+public final class SyncAsyncExecutors {
+
+ public static SyncAsyncExecutor synchronousExecutor() {
+ return new SyncAsyncExecutor() {
+ @Override
+ public ExecutionType execute(Consumer runnable, Runnable abortionFallback) {
+ runnable.accept(SYNCHRONOUS);
+ return SYNCHRONOUS;
+ }
+
+ @Override
+ public boolean hasExecutedAllSynchronously() {
+ return true;
+ }
+ };
+ }
+
+ public static SyncAsyncExecutor asynchronousExecutor() {
+
+ Executor executor = Executors.newSingleThreadExecutor();
+
+ return new SyncAsyncExecutor() {
+ @Override
+ public ExecutionType execute(Consumer runnable, Runnable abortionFallback) {
+ executor.execute(() -> runnable.accept(ASYNCHRONOUS));
+ return ASYNCHRONOUS;
+ }
+
+ @Override
+ public boolean hasExecutedAllSynchronously() {
+ return true;
+ }
+ };
+ }
+
+ public static AsyncExecutorStepper stepperAsynchronousExecutor() {
+ return new AsyncExecutorStepper() {
+
+ Executor executor = Executors.newSingleThreadExecutor();
+ Semaphore enterSemaphore = new Semaphore(0);
+ Semaphore exitSemaphore = new Semaphore(0);
+ boolean timedOut = false;
+
+ @Override
+ public void close() {
+ enterSemaphore.release(Integer.MAX_VALUE/2);
+ exitSemaphore.release(Integer.MAX_VALUE/2);
+ }
+
+ @Override
+ public ExecutionType execute(Consumer runnable, Runnable abortionFallback) {
+ executor.execute(() -> {
+ try {
+ enterSemaphore.acquire();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ if (timedOut) {
+ abortionFallback.run();
+ } else {
+ runnable.accept(ASYNCHRONOUS);
+ exitSemaphore.release();
+ }
+ });
+ return ASYNCHRONOUS;
+ }
+
+ @Override
+ public void next() {
+ enterSemaphore.release();
+ try {
+ exitSemaphore.acquire();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public void timeout() {
+ timedOut = true;
+ close();
+ }
+
+ @Override
+ public boolean hasExecutedAllSynchronously() {
+ return true;
+ }
+ };
+ }
+
+ public interface AsyncExecutorStepper extends SyncAsyncExecutor, Closeable {
+ void next();
+
+ void timeout();
+ }
+}
diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts
index 4e0e7fe188..dce6947622 100644
--- a/scm-ui/ui-types/src/Sources.ts
+++ b/scm-ui/ui-types/src/Sources.ts
@@ -13,9 +13,11 @@ export type File = {
directory: boolean;
description?: string;
revision: string;
- length: number;
- lastModified?: string;
+ length?: number;
+ commitDate?: string;
subRepository?: SubRepository; // TODO
+ partialResult: boolean;
+ computationAborted: boolean;
_links: Links;
_embedded: {
children: File[] | null | undefined;
diff --git a/scm-ui/ui-webapp/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/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx
index bfcb02e9bb..c1384b5192 100644
--- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx
+++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx
@@ -7,7 +7,7 @@ import styled from "styled-components";
import { binder } from "@scm-manager/ui-extensions";
import { Repository, File } from "@scm-manager/ui-types";
import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components";
-import { getFetchSourcesFailure, isFetchSourcesPending, getSources } from "../modules/sources";
+import { getFetchSourcesFailure, isFetchSourcesPending, getSources, fetchSources } from "../modules/sources";
import FileTreeLeaf from "./FileTreeLeaf";
type Props = WithTranslation & {
@@ -19,10 +19,16 @@ type Props = WithTranslation & {
path: string;
baseUrl: string;
+ updateSources: () => void;
+
// context props
match: any;
};
+type State = {
+ stoppableUpdateHandler?: number;
+};
+
const FixedWidthTh = styled.th`
width: 16px;
`;
@@ -39,7 +45,28 @@ export function findParent(path: string) {
return "";
}
-class FileTree extends React.Component {
+class FileTree extends React.Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = {};
+ }
+
+ componentDidUpdate(prevProps: Readonly, prevState: Readonly): void {
+ if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) {
+ const { tree, updateSources } = this.props;
+ if (tree?._embedded?.children && tree._embedded.children.find(c => c.partialResult)) {
+ const stoppableUpdateHandler = setTimeout(updateSources, 3000);
+ this.setState({ stoppableUpdateHandler: stoppableUpdateHandler });
+ }
+ }
+ }
+
+ componentWillUnmount(): void {
+ if (this.state.stoppableUpdateHandler) {
+ clearTimeout(this.state.stoppableUpdateHandler);
+ }
+ }
+
render() {
const { error, loading, tree } = this.props;
@@ -106,7 +133,7 @@ class FileTree extends React.Component {