merge with develop

This commit is contained in:
Eduard Heimbuch
2020-04-07 08:31:53 +02:00
18 changed files with 2180 additions and 1712 deletions

View File

@@ -5,12 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## Unreleased
### Added
- Fire various plugin events ([#1088](https://github.com/scm-manager/scm-manager/pull/1088))
- Display version for plugins ([#1089](https://github.com/scm-manager/scm-manager/pull/1089)
### Changed ### Changed
- Simplified collapse state management of the secondary navigation ([#1086](https://github.com/scm-manager/scm-manager/pull/1086) - Simplified collapse state management of the secondary navigation ([#1086](https://github.com/scm-manager/scm-manager/pull/1086)
- Ensure same monospace font-family throughout whole SCM-Manager ([#1091](https://github.com/scm-manager/scm-manager/pull/1091) - Ensure same monospace font-family throughout whole SCM-Manager ([#1091](https://github.com/scm-manager/scm-manager/pull/1091)
### Fixed ### Fixed
- Authentication for write requests for repositories with anonymous read access ([#108](https://github.com/scm-manager/scm-manager/pull/1081)) - Authentication for write requests for repositories with anonymous read access ([#108](https://github.com/scm-manager/scm-manager/pull/1081))
- Submodules in git do no longer lead to a server error in the browser command ([#1093](https://github.com/scm-manager/scm-manager/pull/1093))
## 2.0.0-rc6 - 2020-03-26 ## 2.0.0-rc6 - 2020-03-26
### Added ### Added

View File

@@ -0,0 +1,30 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import sonia.scm.event.Event;
@Event
public class PluginCenterErrorEvent {}

View File

@@ -0,0 +1,43 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import lombok.Getter;
import sonia.scm.event.Event;
@Getter
@Event
public class PluginEvent {
private final PluginEventType eventType;
private final AvailablePlugin plugin;
public PluginEvent(PluginEventType eventType, AvailablePlugin plugin) {
this.eventType = eventType;
this.plugin = plugin;
}
public enum PluginEventType {
INSTALLED, INSTALLATION_FAILED
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
@@ -104,6 +104,12 @@ public class GitBrowseCommand extends AbstractGitCommand
private BrowserResult browserResult; private BrowserResult browserResult;
private BrowseCommandRequest request;
private org.eclipse.jgit.lib.Repository repo;
private ObjectId revId;
private int resultCount = 0; private int resultCount = 0;
public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) { public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) {
@@ -117,11 +123,12 @@ public class GitBrowseCommand extends AbstractGitCommand
throws IOException { throws IOException {
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(); this.request = request;
ObjectId revId = computeRevIdToBrowse(request, repo); repo = open();
revId = computeRevIdToBrowse();
if (revId != null) { if (revId != null) {
browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry());
return browserResult; return browserResult;
} else { } else {
logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName());
@@ -129,7 +136,7 @@ public class GitBrowseCommand extends AbstractGitCommand
} }
} }
private ObjectId computeRevIdToBrowse(BrowseCommandRequest request, org.eclipse.jgit.lib.Repository repo) throws IOException { private ObjectId computeRevIdToBrowse() throws IOException {
if (Util.isEmpty(request.getRevision())) { if (Util.isEmpty(request.getRevision())) {
return getDefaultBranch(repo); return getDefaultBranch(repo);
} else { } else {
@@ -151,9 +158,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return fileObject; return fileObject;
} }
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, private FileObject createFileObject(TreeEntry treeEntry) throws IOException {
BrowseCommandRequest request, ObjectId revId, TreeEntry treeEntry)
throws IOException {
FileObject file = new FileObject(); FileObject file = new FileObject();
@@ -162,18 +167,11 @@ public class GitBrowseCommand extends AbstractGitCommand
file.setName(treeEntry.getNameString()); file.setName(treeEntry.getNameString());
file.setPath(path); file.setPath(path);
SubRepository sub = null; if (treeEntry.getType() == TreeType.SUB_REPOSITORY)
if (!request.isDisableSubRepositoryDetection())
{
sub = getSubRepository(repo, revId, path);
}
if (sub != null)
{ {
logger.trace("{} seems to be a sub repository", path); logger.trace("{} seems to be a sub repository", path);
file.setDirectory(true); file.setDirectory(true);
file.setSubRepository(sub); file.setSubRepository(treeEntry.subRepository);
} }
else else
{ {
@@ -189,7 +187,7 @@ public class GitBrowseCommand extends AbstractGitCommand
try (RevWalk walk = new RevWalk(repo)) { try (RevWalk walk = new RevWalk(repo)) {
commit = walk.parseCommit(revId); commit = walk.parseCommit(revId);
} }
Optional<LfsPointer> lfsPointer = getLfsPointer(repo, path, commit, treeEntry); Optional<LfsPointer> lfsPointer = getLfsPointer(path, commit, treeEntry);
if (lfsPointer.isPresent()) { if (lfsPointer.isPresent()) {
setFileLengthFromLfsBlob(lfsPointer.get(), file); setFileLengthFromLfsBlob(lfsPointer.get(), file);
@@ -198,24 +196,24 @@ public class GitBrowseCommand extends AbstractGitCommand
} }
executor.execute( executor.execute(
new CompleteFileInformation(path, revId, repo, file, request), new CompleteFileInformation(path, file),
new AbortFileInformation(request) new AbortFileInformation()
); );
} }
} }
return file; return file;
} }
private void updateCache(BrowseCommandRequest request) { private void updateCache() {
request.updateCache(browserResult); request.updateCache(browserResult);
logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); logger.info("updated browser result for repository {}", repository.getNamespaceAndName());
} }
private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException { private FileObject getEntry() throws IOException {
try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo)) { try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo)) {
logger.debug("load repository browser for revision {}", revId.name()); logger.debug("load repository browser for revision {}", revId.name());
if (!isRootRequest(request)) { if (!isRootRequest()) {
treeWalk.setFilter(PathFilter.create(request.getPath())); treeWalk.setFilter(PathFilter.create(request.getPath()));
} }
@@ -227,46 +225,46 @@ public class GitBrowseCommand extends AbstractGitCommand
throw new IllegalStateException("could not find tree for " + revId.name()); throw new IllegalStateException("could not find tree for " + revId.name());
} }
if (isRootRequest(request)) { if (isRootRequest()) {
FileObject result = createEmptyRoot(); FileObject result = createEmptyRoot();
findChildren(result, repo, request, revId, treeWalk); findChildren(result, treeWalk);
return result; return result;
} else { } else {
FileObject result = findFirstMatch(repo, request, revId, treeWalk); FileObject result = findFirstMatch(treeWalk);
if ( result.isDirectory() ) { if ( result.isDirectory() ) {
treeWalk.enterSubtree(); treeWalk.enterSubtree();
findChildren(result, repo, request, revId, treeWalk); findChildren(result, treeWalk);
} }
return result; return result;
} }
} }
} }
private boolean isRootRequest(BrowseCommandRequest request) { private boolean isRootRequest() {
return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath()); return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath());
} }
private void findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { private void findChildren(FileObject parent, TreeWalk treeWalk) throws IOException {
TreeEntry entry = new TreeEntry(); TreeEntry entry = new TreeEntry();
createTree(parent.getPath(), entry, repo, request, treeWalk); createTree(parent.getPath(), entry, treeWalk);
convertToFileObject(parent, repo, request, revId, entry.getChildren()); convertToFileObject(parent, entry.getChildren());
} }
private void convertToFileObject(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, List<TreeEntry> entries) throws IOException { private void convertToFileObject(FileObject parent, List<TreeEntry> entries) throws IOException {
List<FileObject> files = Lists.newArrayList(); List<FileObject> files = Lists.newArrayList();
Iterator<TreeEntry> entryIterator = entries.iterator(); Iterator<TreeEntry> entryIterator = entries.iterator();
boolean hasNext; boolean hasNext;
while ((hasNext = entryIterator.hasNext()) && resultCount < request.getLimit() + request.getOffset()) while ((hasNext = entryIterator.hasNext()) && resultCount < request.getLimit() + request.getOffset())
{ {
TreeEntry entry = entryIterator.next(); TreeEntry entry = entryIterator.next();
FileObject fileObject = createFileObject(repo, request, revId, entry); FileObject fileObject = createFileObject(entry);
if (!fileObject.isDirectory()) { if (!fileObject.isDirectory()) {
++resultCount; ++resultCount;
} }
if (request.isRecursive() && fileObject.isDirectory()) { if (request.isRecursive() && fileObject.isDirectory()) {
convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); convertToFileObject(fileObject, entry.getChildren());
} }
if (resultCount > request.getOffset() || (request.getOffset() == 0 && fileObject.isDirectory())) { if (resultCount > request.getOffset() || (request.getOffset() == 0 && fileObject.isDirectory())) {
@@ -279,7 +277,7 @@ public class GitBrowseCommand extends AbstractGitCommand
parent.setTruncated(hasNext); parent.setTruncated(hasNext);
} }
private Optional<TreeEntry> createTree(String path, TreeEntry parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, TreeWalk treeWalk) throws IOException { private Optional<TreeEntry> createTree(String path, TreeEntry parent, TreeWalk treeWalk) throws IOException {
List<TreeEntry> entries = new ArrayList<>(); List<TreeEntry> entries = new ArrayList<>();
while (treeWalk.next()) { while (treeWalk.next()) {
TreeEntry treeEntry = new TreeEntry(repo, treeWalk); TreeEntry treeEntry = new TreeEntry(repo, treeWalk);
@@ -290,9 +288,9 @@ public class GitBrowseCommand extends AbstractGitCommand
entries.add(treeEntry); entries.add(treeEntry);
if (request.isRecursive() && treeEntry.isDirectory()) { if (request.isRecursive() && treeEntry.getType() == TreeType.DIRECTORY) {
treeWalk.enterSubtree(); treeWalk.enterSubtree();
Optional<TreeEntry> surplus = createTree(treeEntry.getNameString(), treeEntry, repo, request, treeWalk); Optional<TreeEntry> surplus = createTree(treeEntry.getNameString(), treeEntry, treeWalk);
surplus.ifPresent(entries::add); surplus.ifPresent(entries::add);
} }
} }
@@ -300,8 +298,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return empty(); return empty();
} }
private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo, private FileObject findFirstMatch(TreeWalk treeWalk) throws IOException {
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException {
String[] pathElements = request.getPath().split("/"); String[] pathElements = request.getPath().split("/");
int currentDepth = 0; int currentDepth = 0;
int limit = pathElements.length; int limit = pathElements.length;
@@ -313,7 +310,7 @@ public class GitBrowseCommand extends AbstractGitCommand
currentDepth++; currentDepth++;
if (currentDepth >= limit) { if (currentDepth >= limit) {
return createFileObject(repo, request, revId, new TreeEntry(repo, treeWalk)); return createFileObject(new TreeEntry(repo, treeWalk));
} else { } else {
treeWalk.enterSubtree(); treeWalk.enterSubtree();
} }
@@ -323,13 +320,13 @@ public class GitBrowseCommand extends AbstractGitCommand
throw notFound(entity("File", request.getPath()).in("Revision", revId.getName()).in(this.repository)); throw notFound(entity("File", request.getPath()).in("Revision", revId.getName()).in(this.repository));
} }
private Map<String, SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo, ObjectId revision) private Map<String, SubRepository> getSubRepositories()
throws IOException { throws IOException {
logger.debug("read submodules of {} at {}", repository.getName(), revision); logger.debug("read submodules of {} at {}", repository.getName(), revId);
try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) { try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) {
new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision, new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revId,
PATH_MODULES, baos); PATH_MODULES, baos);
return GitSubModuleParser.parse(baos.toString()); return GitSubModuleParser.parse(baos.toString());
} catch (NotFoundException ex) { } catch (NotFoundException ex) {
@@ -338,12 +335,12 @@ public class GitBrowseCommand extends AbstractGitCommand
} }
} }
private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path) private SubRepository getSubRepository(String path)
throws IOException { throws IOException {
Map<String, SubRepository> subRepositories = subrepositoryCache.get(revId); Map<String, SubRepository> subRepositories = subrepositoryCache.get(revId);
if (subRepositories == null) { if (subRepositories == null) {
subRepositories = getSubRepositories(repo, revId); subRepositories = getSubRepositories();
subrepositoryCache.put(revId, subRepositories); subrepositoryCache.put(revId, subRepositories);
} }
@@ -353,7 +350,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return null; return null;
} }
private Optional<LfsPointer> getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeEntry treeWalk) { private Optional<LfsPointer> getLfsPointer(String path, RevCommit commit, TreeEntry treeWalk) {
try { try {
Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit); Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit);
@@ -377,17 +374,11 @@ public class GitBrowseCommand extends AbstractGitCommand
private class CompleteFileInformation implements Consumer<SyncAsyncExecutor.ExecutionType> { private class CompleteFileInformation implements Consumer<SyncAsyncExecutor.ExecutionType> {
private final String path; private final String path;
private final ObjectId revId;
private final org.eclipse.jgit.lib.Repository repo;
private final FileObject file; private final FileObject file;
private final BrowseCommandRequest request;
public CompleteFileInformation(String path, ObjectId revId, org.eclipse.jgit.lib.Repository repo, FileObject file, BrowseCommandRequest request) { public CompleteFileInformation(String path, FileObject file) {
this.path = path; this.path = path;
this.revId = revId;
this.repo = repo;
this.file = file; this.file = file;
this.request = request;
} }
@Override @Override
@@ -429,23 +420,18 @@ public class GitBrowseCommand extends AbstractGitCommand
file.setCommitDate(GitUtil.getCommitTime(commit)); file.setCommitDate(GitUtil.getCommitTime(commit));
file.setDescription(commit.getShortMessage()); file.setDescription(commit.getShortMessage());
if (executionType == ASYNCHRONOUS && browserResult != null) { if (executionType == ASYNCHRONOUS && browserResult != null) {
updateCache(request); updateCache();
} }
} }
} }
private class AbortFileInformation implements Runnable { private class AbortFileInformation implements Runnable {
private final BrowseCommandRequest request;
public AbortFileInformation(BrowseCommandRequest request) {
this.request = request;
}
@Override @Override
public void run() { public void run() {
synchronized (asyncMonitor) { synchronized (asyncMonitor) {
if (markPartialAsAborted(browserResult.getFile())) { if (markPartialAsAborted(browserResult.getFile())) {
updateCache(request); updateCache();
} }
} }
} }
@@ -464,28 +450,42 @@ public class GitBrowseCommand extends AbstractGitCommand
} }
} }
private enum TreeType {
FILE, DIRECTORY, SUB_REPOSITORY
}
private class TreeEntry { private class TreeEntry {
private final String pathString; private final String pathString;
private final String nameString; private final String nameString;
private final ObjectId objectId; private final ObjectId objectId;
private final boolean directory; private final TreeType type;
private final SubRepository subRepository;
private List<TreeEntry> children = emptyList(); private List<TreeEntry> children = emptyList();
TreeEntry() { TreeEntry() {
pathString = ""; pathString = "";
nameString = ""; nameString = "";
objectId = null; objectId = null;
directory = true; subRepository = null;
type = TreeType.DIRECTORY;
} }
TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException {
this.pathString = treeWalk.getPathString(); this.pathString = treeWalk.getPathString();
this.nameString = treeWalk.getNameString(); this.nameString = treeWalk.getNameString();
this.objectId = treeWalk.getObjectId(0); this.objectId = treeWalk.getObjectId(0);
ObjectLoader loader = repo.open(objectId);
this.directory = loader.getType() == Constants.OBJ_TREE; if (!request.isDisableSubRepositoryDetection() && GitBrowseCommand.this.getSubRepository(pathString) != null) {
subRepository = GitBrowseCommand.this.getSubRepository(pathString);
type = TreeType.SUB_REPOSITORY;
} else if (repo.open(objectId).getType() == Constants.OBJ_TREE) {
subRepository = null;
type = TreeType.DIRECTORY;
} else {
subRepository = null;
type = TreeType.FILE;
}
} }
String getPathString() { String getPathString() {
@@ -500,8 +500,12 @@ public class GitBrowseCommand extends AbstractGitCommand
return objectId; return objectId;
} }
boolean isDirectory() { SubRepository getSubRepository() {
return directory; return subRepository;
}
TreeType getType() {
return type;
} }
List<TreeEntry> getChildren() { List<TreeEntry> getChildren() {
@@ -509,7 +513,7 @@ public class GitBrowseCommand extends AbstractGitCommand
} }
void setChildren(List<TreeEntry> children) { void setChildren(List<TreeEntry> children) {
sort(children, TreeEntry::isDirectory, TreeEntry::getNameString); sort(children, entry -> entry.type != TreeType.FILE, TreeEntry::getNameString);
this.children = children; this.children = children;
} }
} }

View File

@@ -0,0 +1,56 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { MemoryRouter } from "react-router-dom";
import { storiesOf } from "@storybook/react";
import CardColumn from "./CardColumn";
import Icon from "./Icon";
import styled from "styled-components";
const Wrapper = styled.div`
margin: 2rem;
max-width: 400px;
`;
const Container: FC = ({ children }) => <Wrapper>{children}</Wrapper>;
const title = <strong>title</strong>;
const avatar = <Icon name="icons fa-2x" className="media-left" />;
const link = "/foo/bar";
const footerLeft = <small>left footer</small>;
const footerRight = <small>right footer</small>;
storiesOf("CardColumn", module)
.addDecorator(story => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.add("default", () => (
<CardColumn
title={title}
description="A description can be added here."
avatar={avatar}
link={link}
footerLeft={footerLeft}
footerRight={footerRight}
/>
));

View File

@@ -68,6 +68,14 @@ const ContentRight = styled.div`
margin-left: auto; margin-left: auto;
`; `;
const RightMarginDiv = styled.div`
margin-right: 0.5rem;
`;
const InheritFlexShrinkDiv = styled.div`
flex-shrink: inherit;
`;
export default class CardColumn extends React.Component<Props> { export default class CardColumn extends React.Component<Props> {
createLink = () => { createLink = () => {
const { link, action } = this.props; const { link, action } = this.props;
@@ -105,8 +113,10 @@ export default class CardColumn extends React.Component<Props> {
<ContentRight>{contentRight}</ContentRight> <ContentRight>{contentRight}</ContentRight>
</div> </div>
<FooterWrapper className={classNames("level", "is-flex")}> <FooterWrapper className={classNames("level", "is-flex")}>
<div className="level-left is-hidden-mobile">{footerLeft}</div> <RightMarginDiv className="level-left is-hidden-mobile">{footerLeft}</RightMarginDiv>
<div className="level-right is-mobile is-marginless">{footerRight}</div> <InheritFlexShrinkDiv className="level-right is-block is-mobile is-marginless shorten-text">
{footerRight}
</InheritFlexShrinkDiv>
</FooterWrapper> </FooterWrapper>
</FlexFullHeight> </FlexFullHeight>
</NoEventWrapper> </NoEventWrapper>

View File

@@ -0,0 +1,48 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { MemoryRouter } from "react-router-dom";
import { storiesOf } from "@storybook/react";
import CardColumnSmall from "./CardColumnSmall";
import Icon from "./Icon";
import styled from "styled-components";
const Wrapper = styled.div`
margin: 2rem;
max-width: 400px;
`;
const Container: FC = ({ children }) => <Wrapper>{children}</Wrapper>;
const link = "/foo/bar";
const icon = <Icon name="icons fa-2x" className="media-left" />;
const contentLeft = <strong className="is-marginless">main content</strong>;
const contentRight = <small>more text</small>;
storiesOf("CardColumnSmall", module)
.addDecorator(story => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
.addDecorator(storyFn => <Container>{storyFn()}</Container>)
.add("default", () => (
<CardColumnSmall link={link} icon={icon} contentLeft={contentLeft} contentRight={contentRight} />
));

View File

@@ -0,0 +1,82 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, ReactNode } from "react";
import classNames from "classnames";
import styled from "styled-components";
import { Link } from "react-router-dom";
type Props = {
link: string;
icon: ReactNode;
contentLeft: ReactNode;
contentRight: ReactNode;
footer?: ReactNode;
};
const FlexFullHeight = styled.div`
flex-direction: column;
justify-content: space-around;
align-self: stretch;
`;
const ContentLeft = styled.div`
margin-bottom: 0 !important;
overflow: hidden;
`;
const ContentRight = styled.div`
margin-left: auto;
align-items: start;
`;
const CenteredItems = styled.div`
align-items: center;
`;
const StyledLink = styled(Link)`
color: inherit;
:hover {
color: #33b2e8 !important;
}
`;
const CardColumnSmall: FC<Props> = ({ link, icon, contentLeft, contentRight, footer }) => {
return (
<StyledLink to={link}>
<div className="media">
{icon}
<FlexFullHeight className={classNames("media-content", "text-box", "is-flex")}>
<CenteredItems className="is-flex">
<ContentLeft>{contentLeft}</ContentLeft>
<ContentRight>{contentRight}</ContentRight>
</CenteredItems>
<small>{footer}</small>
</FlexFullHeight>
</div>
</StyledLink>
);
};
export default CardColumnSmall;

View File

@@ -445,6 +445,119 @@ exports[`Storyshots Buttons|SubmitButton Default 1`] = `
</div> </div>
`; `;
exports[`Storyshots CardColumn default 1`] = `
<div
className="CardColumnstories__Wrapper-sc-1ztucl-0 IFDjP"
>
<a
className="overlay-column"
href="/foo/bar"
onClick={[Function]}
/>
<article
className="CardColumn__NoEventWrapper-sc-1w6lsih-0 kZKqpc media"
>
<figure
className="CardColumn__AvatarWrapper-sc-1w6lsih-1 bZyfne media-left"
>
<i
className="fas fa-icons fa-2x has-text-grey-light media-left"
/>
</figure>
<div
className="CardColumn__FlexFullHeight-sc-1w6lsih-2 cAdfGj media-content text-box is-flex"
>
<div
className="is-flex"
>
<div
className="CardColumn__ContentLeft-sc-1w6lsih-4 dumWkw content"
>
<p
className="shorten-text is-marginless"
>
<strong>
title
</strong>
</p>
<p
className="shorten-text"
>
A description can be added here.
</p>
</div>
<div
className="CardColumn__ContentRight-sc-1w6lsih-5 kyEPRa"
/>
</div>
<div
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
>
<div
className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
>
<small>
left footer
</small>
</div>
<div
className="CardColumn__InheritFlexShrinkDiv-sc-1w6lsih-7 jkwBTE level-right is-block is-mobile is-marginless shorten-text"
>
<small>
right footer
</small>
</div>
</div>
</div>
</article>
</div>
`;
exports[`Storyshots CardColumnSmall default 1`] = `
<div
className="CardColumnSmallstories__Wrapper-ofr817-0 fwZASP"
>
<a
className="CardColumnSmall__StyledLink-tk9h0o-4 bSCFyE"
href="/foo/bar"
onClick={[Function]}
>
<div
className="media"
>
<i
className="fas fa-icons fa-2x has-text-grey-light media-left"
/>
<div
className="CardColumnSmall__FlexFullHeight-tk9h0o-0 fwRxNw media-content text-box is-flex"
>
<div
className="CardColumnSmall__CenteredItems-tk9h0o-3 eHPOKj is-flex"
>
<div
className="CardColumnSmall__ContentLeft-tk9h0o-1 ihirgF"
>
<strong
className="is-marginless"
>
main content
</strong>
</div>
<div
className="CardColumnSmall__ContentRight-tk9h0o-2 jZRaNn"
>
<small>
more text
</small>
</div>
</div>
<small />
</div>
</div>
</a>
</div>
`;
exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots DateFromNow Default 1`] = `
<div> <div>
<p> <p>
@@ -34455,7 +34568,7 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex" className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
> >
<div <div
className="level-left is-hidden-mobile" className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
> >
<a <a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item" className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item"
@@ -34495,7 +34608,7 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
</a> </a>
</div> </div>
<div <div
className="level-right is-mobile is-marginless" className="CardColumn__InheritFlexShrinkDiv-sc-1w6lsih-7 jkwBTE level-right is-block is-mobile is-marginless shorten-text"
> >
<small <small
className="level-item" className="level-item"
@@ -34572,7 +34685,7 @@ exports[`Storyshots RepositoryEntry Before Title EP 1`] = `
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex" className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
> >
<div <div
className="level-left is-hidden-mobile" className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
> >
<a <a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item" className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item"
@@ -34612,7 +34725,7 @@ exports[`Storyshots RepositoryEntry Before Title EP 1`] = `
</a> </a>
</div> </div>
<div <div
className="level-right is-mobile is-marginless" className="CardColumn__InheritFlexShrinkDiv-sc-1w6lsih-7 jkwBTE level-right is-block is-mobile is-marginless shorten-text"
> >
<small <small
className="level-item" className="level-item"
@@ -34686,7 +34799,7 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex" className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
> >
<div <div
className="level-left is-hidden-mobile" className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
> >
<a <a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item" className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item"
@@ -34726,7 +34839,7 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
</a> </a>
</div> </div>
<div <div
className="level-right is-mobile is-marginless" className="CardColumn__InheritFlexShrinkDiv-sc-1w6lsih-7 jkwBTE level-right is-block is-mobile is-marginless shorten-text"
> >
<small <small
className="level-item" className="level-item"
@@ -34800,7 +34913,7 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex" className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
> >
<div <div
className="level-left is-hidden-mobile" className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
> >
<a <a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item" className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item"
@@ -34847,7 +34960,7 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
</a> </a>
</div> </div>
<div <div
className="level-right is-mobile is-marginless" className="CardColumn__InheritFlexShrinkDiv-sc-1w6lsih-7 jkwBTE level-right is-block is-mobile is-marginless shorten-text"
> >
<small <small
className="level-item" className="level-item"

View File

@@ -75,6 +75,7 @@ export { default as ErrorBoundary } from "./ErrorBoundary";
export { default as OverviewPageActions } from "./OverviewPageActions"; export { default as OverviewPageActions } from "./OverviewPageActions";
export { default as CardColumnGroup } from "./CardColumnGroup"; export { default as CardColumnGroup } from "./CardColumnGroup";
export { default as CardColumn } from "./CardColumn"; export { default as CardColumn } from "./CardColumn";
export { default as CardColumnSmall } from "./CardColumnSmall";
export { default as comparators } from "./comparators"; export { default as comparators } from "./comparators";

View File

@@ -87,8 +87,12 @@ class PluginEntry extends React.Component<Props, State> {
}); });
}; };
createFooterLeft = (plugin: Plugin) => {
return <small>{plugin.version}</small>;
};
createFooterRight = (plugin: Plugin) => { createFooterRight = (plugin: Plugin) => {
return <small className="level-item">{plugin.author}</small>; return <small className="level-item is-block shorten-text">{plugin.author}</small>;
}; };
isInstallable = () => { isInstallable = () => {
@@ -172,8 +176,8 @@ class PluginEntry extends React.Component<Props, State> {
const { plugin } = this.props; const { plugin } = this.props;
const avatar = this.createAvatar(plugin); const avatar = this.createAvatar(plugin);
const actionbar = this.createActionbar(); const actionbar = this.createActionbar();
const footerLeft = this.createFooterLeft(plugin);
const footerRight = this.createFooterRight(plugin); const footerRight = this.createFooterRight(plugin);
const modal = this.renderModal(); const modal = this.renderModal();
return ( return (
@@ -184,6 +188,7 @@ class PluginEntry extends React.Component<Props, State> {
title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>} title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>}
description={plugin.description} description={plugin.description}
contentRight={plugin.pending || plugin.markedForUninstall ? this.createPendingSpinner() : actionbar} contentRight={plugin.pending || plugin.markedForUninstall ? this.createPendingSpinner() : actionbar}
footerLeft={footerLeft}
footerRight={footerRight} footerRight={footerRight}
/> />
{modal} {modal}

View File

@@ -24,7 +24,8 @@
SOFTWARE. SOFTWARE.
--> -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@@ -309,7 +310,7 @@
<dependency> <dependency>
<groupId>org.apache.tika</groupId> <groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId> <artifactId>tika-core</artifactId>
<version>1.23</version> <version>1.24</version>
</dependency> </dependency>
<!-- unix restart --> <!-- unix restart -->
@@ -462,7 +463,6 @@
<version>2.1.1</version> <version>2.1.1</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
@@ -663,7 +663,7 @@
<scm.stage>DEVELOPMENT</scm.stage> <scm.stage>DEVELOPMENT</scm.stage>
<scm.home>target/scm-it</scm.home> <scm.home>target/scm-it</scm.home>
<environment.profile>default</environment.profile> <environment.profile>default</environment.profile>
<jjwt.version>0.11.0</jjwt.version> <jjwt.version>0.11.1</jjwt.version>
<selenium.version>2.53.1</selenium.version> <selenium.version>2.53.1</selenium.version>
<wagon.version>1.0</wagon.version> <wagon.version>1.0</wagon.version>
<mustache.version>0.9.6-scm1</mustache.version> <mustache.version>0.9.6-scm1</mustache.version>

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.plugin; package sonia.scm.plugin;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
@@ -31,7 +31,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.lifecycle.RestartEvent;
import sonia.scm.lifecycle.Restarter; import sonia.scm.lifecycle.Restarter;
import sonia.scm.version.Version; import sonia.scm.version.Version;
@@ -52,7 +51,6 @@ import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@Singleton @Singleton
@@ -64,17 +62,19 @@ public class DefaultPluginManager implements PluginManager {
private final PluginCenter center; private final PluginCenter center;
private final PluginInstaller installer; private final PluginInstaller installer;
private final Restarter restarter; private final Restarter restarter;
private final ScmEventBus eventBus;
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>(); private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>(); private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
@Inject @Inject
public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter) { public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus) {
this.loader = loader; this.loader = loader;
this.center = center; this.center = center;
this.installer = installer; this.installer = installer;
this.restarter = restarter; this.restarter = restarter;
this.eventBus = eventBus;
this.computeInstallationDependencies(); this.computeInstallationDependencies();
} }
@@ -172,8 +172,10 @@ public class DefaultPluginManager implements PluginManager {
PendingPluginInstallation pending = installer.install(plugin); PendingPluginInstallation pending = installer.install(plugin);
dependencyTracker.addInstalled(plugin.getDescriptor()); dependencyTracker.addInstalled(plugin.getDescriptor());
pendingInstallations.add(pending); pendingInstallations.add(pending);
eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLED, plugin));
} catch (PluginInstallException ex) { } catch (PluginInstallException ex) {
cancelPending(pendingInstallations); cancelPending(pendingInstallations);
eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLATION_FAILED, plugin));
throw ex; throw ex;
} }
} }
@@ -255,7 +257,7 @@ public class DefaultPluginManager implements PluginManager {
Set<String> dependencies = plugin.getDescriptor().getDependencies(); Set<String> dependencies = plugin.getDescriptor().getDependencies();
if (dependencies != null) { if (dependencies != null) {
for (String dependency: dependencies){ for (String dependency : dependencies) {
collectPluginsToInstall(plugins, dependency, false); collectPluginsToInstall(plugins, dependency, false);
} }
} }

View File

@@ -27,10 +27,10 @@ package sonia.scm.plugin;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.net.ahc.AdvancedHttpClient;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
@@ -40,16 +40,18 @@ class PluginCenterLoader {
private final AdvancedHttpClient client; private final AdvancedHttpClient client;
private final PluginCenterDtoMapper mapper; private final PluginCenterDtoMapper mapper;
private final ScmEventBus eventBus;
@Inject @Inject
public PluginCenterLoader(AdvancedHttpClient client) { public PluginCenterLoader(AdvancedHttpClient client, ScmEventBus eventBus) {
this(client, PluginCenterDtoMapper.INSTANCE); this(client, PluginCenterDtoMapper.INSTANCE, eventBus);
} }
@VisibleForTesting @VisibleForTesting
PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper) { PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper, ScmEventBus eventBus) {
this.client = client; this.client = client;
this.mapper = mapper; this.mapper = mapper;
this.eventBus = eventBus;
} }
Set<AvailablePlugin> load(String url) { Set<AvailablePlugin> load(String url) {
@@ -59,8 +61,8 @@ class PluginCenterLoader {
return mapper.map(pluginCenterDto); return mapper.map(pluginCenterDto);
} catch (Exception ex) { } catch (Exception ex) {
LOG.error("failed to load plugins from plugin center, returning empty list", ex); LOG.error("failed to load plugins from plugin center, returning empty list", ex);
eventBus.post(new PluginCenterErrorEvent());
return Collections.emptySet(); return Collections.emptySet();
} }
} }
} }

View File

@@ -31,22 +31,18 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.lifecycle.Restarter; import sonia.scm.lifecycle.Restarter;
import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.AvailablePluginDescriptor;
import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginInformation;
import java.net.URI; import java.net.URI;
import java.util.Collections;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginTestHelper.createAvailable; import static sonia.scm.plugin.PluginTestHelper.createAvailable;
import static sonia.scm.plugin.PluginTestHelper.createInstalled; import static sonia.scm.plugin.PluginTestHelper.createInstalled;

View File

@@ -36,16 +36,19 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory; import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.ScmConstraintViolationException; import sonia.scm.ScmConstraintViolationException;
import sonia.scm.event.ScmEventBus;
import sonia.scm.lifecycle.Restarter; import sonia.scm.lifecycle.Restarter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -53,6 +56,7 @@ import static java.util.Arrays.asList;
import static java.util.Collections.singleton; import static java.util.Collections.singleton;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.in;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
@@ -83,6 +87,12 @@ class DefaultPluginManagerTest {
@Mock @Mock
private Restarter restarter; private Restarter restarter;
@Mock
private ScmEventBus eventBus;
@Captor
private ArgumentCaptor<PluginEvent> eventCaptor;
@InjectMocks @InjectMocks
private DefaultPluginManager manager; private DefaultPluginManager manager;
@@ -537,8 +547,36 @@ class DefaultPluginManagerTest {
verify(installer, never()).install(oldScriptPlugin); verify(installer, never()).install(oldScriptPlugin);
} }
@Test
void shouldFirePluginEventOnInstallation() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false);
verify(eventBus).post(eventCaptor.capture());
assertThat(eventCaptor.getValue().getEventType()).isEqualTo(PluginEvent.PluginEventType.INSTALLED);
assertThat(eventCaptor.getValue().getPlugin()).isEqualTo(review);
}
@Test
void shouldFirePluginEventOnFailedInstallation() {
AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(review);
assertThrows(PluginDownloadException.class, () -> manager.install("scm-review-plugin", false));
verify(eventBus).post(eventCaptor.capture());
assertThat(eventCaptor.getValue().getEventType()).isEqualTo(PluginEvent.PluginEventType.INSTALLATION_FAILED);
assertThat(eventCaptor.getValue().getPlugin()).isEqualTo(review);
}
} }
@Nested @Nested
class WithoutReadPermissions { class WithoutReadPermissions {

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.plugin; package sonia.scm.plugin;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -30,6 +30,7 @@ import org.mockito.Answers;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.net.ahc.AdvancedHttpClient;
import java.io.IOException; import java.io.IOException;
@@ -37,6 +38,8 @@ import java.util.Collections;
import java.util.Set; import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@@ -50,6 +53,9 @@ class PluginCenterLoaderTest {
@Mock @Mock
private PluginCenterDtoMapper mapper; private PluginCenterDtoMapper mapper;
@Mock
private ScmEventBus eventBus;
@InjectMocks @InjectMocks
private PluginCenterLoader loader; private PluginCenterLoader loader;
@@ -71,4 +77,13 @@ class PluginCenterLoaderTest {
Set<AvailablePlugin> fetch = loader.load(PLUGIN_URL); Set<AvailablePlugin> fetch = loader.load(PLUGIN_URL);
assertThat(fetch).isEmpty(); assertThat(fetch).isEmpty();
} }
@Test
void shouldFirePluginCenterErrorEvent() throws IOException {
when(client.get(PLUGIN_URL).request()).thenThrow(new IOException("failed to fetch"));
loader.load(PLUGIN_URL);
verify(eventBus).post(any(PluginCenterErrorEvent.class));
}
} }

3246
yarn.lock

File diff suppressed because it is too large Load Diff