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).
## 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
- 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)
### Fixed
- 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
### 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

@@ -104,6 +104,12 @@ public class GitBrowseCommand extends AbstractGitCommand
private BrowserResult browserResult;
private BrowseCommandRequest request;
private org.eclipse.jgit.lib.Repository repo;
private ObjectId revId;
private int resultCount = 0;
public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) {
@@ -117,11 +123,12 @@ public class GitBrowseCommand extends AbstractGitCommand
throws IOException {
logger.debug("try to create browse result for {}", request);
org.eclipse.jgit.lib.Repository repo = open();
ObjectId revId = computeRevIdToBrowse(request, repo);
this.request = request;
repo = open();
revId = computeRevIdToBrowse();
if (revId != null) {
browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId));
browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry());
return browserResult;
} else {
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())) {
return getDefaultBranch(repo);
} else {
@@ -151,9 +158,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return fileObject;
}
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeEntry treeEntry)
throws IOException {
private FileObject createFileObject(TreeEntry treeEntry) throws IOException {
FileObject file = new FileObject();
@@ -162,18 +167,11 @@ public class GitBrowseCommand extends AbstractGitCommand
file.setName(treeEntry.getNameString());
file.setPath(path);
SubRepository sub = null;
if (!request.isDisableSubRepositoryDetection())
{
sub = getSubRepository(repo, revId, path);
}
if (sub != null)
if (treeEntry.getType() == TreeType.SUB_REPOSITORY)
{
logger.trace("{} seems to be a sub repository", path);
file.setDirectory(true);
file.setSubRepository(sub);
file.setSubRepository(treeEntry.subRepository);
}
else
{
@@ -189,7 +187,7 @@ public class GitBrowseCommand extends AbstractGitCommand
try (RevWalk walk = new RevWalk(repo)) {
commit = walk.parseCommit(revId);
}
Optional<LfsPointer> lfsPointer = getLfsPointer(repo, path, commit, treeEntry);
Optional<LfsPointer> lfsPointer = getLfsPointer(path, commit, treeEntry);
if (lfsPointer.isPresent()) {
setFileLengthFromLfsBlob(lfsPointer.get(), file);
@@ -198,24 +196,24 @@ public class GitBrowseCommand extends AbstractGitCommand
}
executor.execute(
new CompleteFileInformation(path, revId, repo, file, request),
new AbortFileInformation(request)
new CompleteFileInformation(path, file),
new AbortFileInformation()
);
}
}
return file;
}
private void updateCache(BrowseCommandRequest request) {
private void updateCache() {
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 {
private FileObject getEntry() throws IOException {
try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo)) {
logger.debug("load repository browser for revision {}", revId.name());
if (!isRootRequest(request)) {
if (!isRootRequest()) {
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());
}
if (isRootRequest(request)) {
if (isRootRequest()) {
FileObject result = createEmptyRoot();
findChildren(result, repo, request, revId, treeWalk);
findChildren(result, treeWalk);
return result;
} else {
FileObject result = findFirstMatch(repo, request, revId, treeWalk);
FileObject result = findFirstMatch(treeWalk);
if ( result.isDirectory() ) {
treeWalk.enterSubtree();
findChildren(result, repo, request, revId, treeWalk);
findChildren(result, treeWalk);
}
return result;
}
}
}
private boolean isRootRequest(BrowseCommandRequest request) {
private boolean isRootRequest() {
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();
createTree(parent.getPath(), entry, repo, request, treeWalk);
convertToFileObject(parent, repo, request, revId, entry.getChildren());
createTree(parent.getPath(), entry, treeWalk);
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();
Iterator<TreeEntry> entryIterator = entries.iterator();
boolean hasNext;
while ((hasNext = entryIterator.hasNext()) && resultCount < request.getLimit() + request.getOffset())
{
TreeEntry entry = entryIterator.next();
FileObject fileObject = createFileObject(repo, request, revId, entry);
FileObject fileObject = createFileObject(entry);
if (!fileObject.isDirectory()) {
++resultCount;
}
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())) {
@@ -279,7 +277,7 @@ public class GitBrowseCommand extends AbstractGitCommand
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<>();
while (treeWalk.next()) {
TreeEntry treeEntry = new TreeEntry(repo, treeWalk);
@@ -290,9 +288,9 @@ public class GitBrowseCommand extends AbstractGitCommand
entries.add(treeEntry);
if (request.isRecursive() && treeEntry.isDirectory()) {
if (request.isRecursive() && treeEntry.getType() == TreeType.DIRECTORY) {
treeWalk.enterSubtree();
Optional<TreeEntry> surplus = createTree(treeEntry.getNameString(), treeEntry, repo, request, treeWalk);
Optional<TreeEntry> surplus = createTree(treeEntry.getNameString(), treeEntry, treeWalk);
surplus.ifPresent(entries::add);
}
}
@@ -300,8 +298,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return empty();
}
private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException {
private FileObject findFirstMatch(TreeWalk treeWalk) throws IOException {
String[] pathElements = request.getPath().split("/");
int currentDepth = 0;
int limit = pathElements.length;
@@ -313,7 +310,7 @@ public class GitBrowseCommand extends AbstractGitCommand
currentDepth++;
if (currentDepth >= limit) {
return createFileObject(repo, request, revId, new TreeEntry(repo, treeWalk));
return createFileObject(new TreeEntry(repo, treeWalk));
} else {
treeWalk.enterSubtree();
}
@@ -323,13 +320,13 @@ public class GitBrowseCommand extends AbstractGitCommand
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 {
logger.debug("read submodules of {} at {}", repository.getName(), revision);
logger.debug("read submodules of {} at {}", repository.getName(), revId);
try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) {
new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision,
new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revId,
PATH_MODULES, baos);
return GitSubModuleParser.parse(baos.toString());
} 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 {
Map<String, SubRepository> subRepositories = subrepositoryCache.get(revId);
if (subRepositories == null) {
subRepositories = getSubRepositories(repo, revId);
subRepositories = getSubRepositories();
subrepositoryCache.put(revId, subRepositories);
}
@@ -353,7 +350,7 @@ public class GitBrowseCommand extends AbstractGitCommand
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 {
Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit);
@@ -377,17 +374,11 @@ public class GitBrowseCommand extends AbstractGitCommand
private class CompleteFileInformation implements Consumer<SyncAsyncExecutor.ExecutionType> {
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) {
public CompleteFileInformation(String path, FileObject file) {
this.path = path;
this.revId = revId;
this.repo = repo;
this.file = file;
this.request = request;
}
@Override
@@ -429,23 +420,18 @@ public class GitBrowseCommand extends AbstractGitCommand
file.setCommitDate(GitUtil.getCommitTime(commit));
file.setDescription(commit.getShortMessage());
if (executionType == ASYNCHRONOUS && browserResult != null) {
updateCache(request);
updateCache();
}
}
}
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);
updateCache();
}
}
}
@@ -464,28 +450,42 @@ public class GitBrowseCommand extends AbstractGitCommand
}
}
private enum TreeType {
FILE, DIRECTORY, SUB_REPOSITORY
}
private class TreeEntry {
private final String pathString;
private final String nameString;
private final ObjectId objectId;
private final boolean directory;
private final TreeType type;
private final SubRepository subRepository;
private List<TreeEntry> children = emptyList();
TreeEntry() {
pathString = "";
nameString = "";
objectId = null;
directory = true;
subRepository = null;
type = TreeType.DIRECTORY;
}
TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException {
this.pathString = treeWalk.getPathString();
this.nameString = treeWalk.getNameString();
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() {
@@ -500,8 +500,12 @@ public class GitBrowseCommand extends AbstractGitCommand
return objectId;
}
boolean isDirectory() {
return directory;
SubRepository getSubRepository() {
return subRepository;
}
TreeType getType() {
return type;
}
List<TreeEntry> getChildren() {
@@ -509,7 +513,7 @@ public class GitBrowseCommand extends AbstractGitCommand
}
void setChildren(List<TreeEntry> children) {
sort(children, TreeEntry::isDirectory, TreeEntry::getNameString);
sort(children, entry -> entry.type != TreeType.FILE, TreeEntry::getNameString);
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;
`;
const RightMarginDiv = styled.div`
margin-right: 0.5rem;
`;
const InheritFlexShrinkDiv = styled.div`
flex-shrink: inherit;
`;
export default class CardColumn extends React.Component<Props> {
createLink = () => {
const { link, action } = this.props;
@@ -105,8 +113,10 @@ export default class CardColumn extends React.Component<Props> {
<ContentRight>{contentRight}</ContentRight>
</div>
<FooterWrapper className={classNames("level", "is-flex")}>
<div className="level-left is-hidden-mobile">{footerLeft}</div>
<div className="level-right is-mobile is-marginless">{footerRight}</div>
<RightMarginDiv className="level-left is-hidden-mobile">{footerLeft}</RightMarginDiv>
<InheritFlexShrinkDiv className="level-right is-block is-mobile is-marginless shorten-text">
{footerRight}
</InheritFlexShrinkDiv>
</FooterWrapper>
</FlexFullHeight>
</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>
`;
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`] = `
<div>
<p>
@@ -34455,7 +34568,7 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
>
<div
className="level-left is-hidden-mobile"
className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
>
<a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item"
@@ -34495,7 +34608,7 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
</a>
</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
className="level-item"
@@ -34572,7 +34685,7 @@ exports[`Storyshots RepositoryEntry Before Title EP 1`] = `
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
>
<div
className="level-left is-hidden-mobile"
className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
>
<a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item"
@@ -34612,7 +34725,7 @@ exports[`Storyshots RepositoryEntry Before Title EP 1`] = `
</a>
</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
className="level-item"
@@ -34686,7 +34799,7 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
>
<div
className="level-left is-hidden-mobile"
className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
>
<a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item"
@@ -34726,7 +34839,7 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
</a>
</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
className="level-item"
@@ -34800,7 +34913,7 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
className="CardColumn__FooterWrapper-sc-1w6lsih-3 jlTqlS level is-flex"
>
<div
className="level-left is-hidden-mobile"
className="CardColumn__RightMarginDiv-sc-1w6lsih-6 dbLPPh level-left is-hidden-mobile"
>
<a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 gtboNN level-item"
@@ -34847,7 +34960,7 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
</a>
</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
className="level-item"

View File

@@ -75,6 +75,7 @@ export { default as ErrorBoundary } from "./ErrorBoundary";
export { default as OverviewPageActions } from "./OverviewPageActions";
export { default as CardColumnGroup } from "./CardColumnGroup";
export { default as CardColumn } from "./CardColumn";
export { default as CardColumnSmall } from "./CardColumnSmall";
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) => {
return <small className="level-item">{plugin.author}</small>;
return <small className="level-item is-block shorten-text">{plugin.author}</small>;
};
isInstallable = () => {
@@ -172,8 +176,8 @@ class PluginEntry extends React.Component<Props, State> {
const { plugin } = this.props;
const avatar = this.createAvatar(plugin);
const actionbar = this.createActionbar();
const footerLeft = this.createFooterLeft(plugin);
const footerRight = this.createFooterRight(plugin);
const modal = this.renderModal();
return (
@@ -184,6 +188,7 @@ class PluginEntry extends React.Component<Props, State> {
title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>}
description={plugin.description}
contentRight={plugin.pending || plugin.markedForUninstall ? this.createPendingSpinner() : actionbar}
footerLeft={footerLeft}
footerRight={footerRight}
/>
{modal}

View File

@@ -24,7 +24,8 @@
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>
@@ -309,7 +310,7 @@
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>1.23</version>
<version>1.24</version>
</dependency>
<!-- unix restart -->
@@ -462,7 +463,6 @@
<version>2.1.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
@@ -663,7 +663,7 @@
<scm.stage>DEVELOPMENT</scm.stage>
<scm.home>target/scm-it</scm.home>
<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>
<wagon.version>1.0</wagon.version>
<mustache.version>0.9.6-scm1</mustache.version>

View File

@@ -31,7 +31,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.event.ScmEventBus;
import sonia.scm.lifecycle.RestartEvent;
import sonia.scm.lifecycle.Restarter;
import sonia.scm.version.Version;
@@ -52,7 +51,6 @@ import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
@Singleton
@@ -64,17 +62,19 @@ public class DefaultPluginManager implements PluginManager {
private final PluginCenter center;
private final PluginInstaller installer;
private final Restarter restarter;
private final ScmEventBus eventBus;
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
@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.center = center;
this.installer = installer;
this.restarter = restarter;
this.eventBus = eventBus;
this.computeInstallationDependencies();
}
@@ -172,8 +172,10 @@ public class DefaultPluginManager implements PluginManager {
PendingPluginInstallation pending = installer.install(plugin);
dependencyTracker.addInstalled(plugin.getDescriptor());
pendingInstallations.add(pending);
eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLED, plugin));
} catch (PluginInstallException ex) {
cancelPending(pendingInstallations);
eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLATION_FAILED, plugin));
throw ex;
}
}

View File

@@ -27,10 +27,10 @@ package sonia.scm.plugin;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
@@ -40,16 +40,18 @@ class PluginCenterLoader {
private final AdvancedHttpClient client;
private final PluginCenterDtoMapper mapper;
private final ScmEventBus eventBus;
@Inject
public PluginCenterLoader(AdvancedHttpClient client) {
this(client, PluginCenterDtoMapper.INSTANCE);
public PluginCenterLoader(AdvancedHttpClient client, ScmEventBus eventBus) {
this(client, PluginCenterDtoMapper.INSTANCE, eventBus);
}
@VisibleForTesting
PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper) {
PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper, ScmEventBus eventBus) {
this.client = client;
this.mapper = mapper;
this.eventBus = eventBus;
}
Set<AvailablePlugin> load(String url) {
@@ -59,8 +61,8 @@ class PluginCenterLoader {
return mapper.map(pluginCenterDto);
} catch (Exception ex) {
LOG.error("failed to load plugins from plugin center, returning empty list", ex);
eventBus.post(new PluginCenterErrorEvent());
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.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.lifecycle.Restarter;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.AvailablePluginDescriptor;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginInformation;
import java.net.URI;
import java.util.Collections;
import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginTestHelper.createAvailable;
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.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.NotFoundException;
import sonia.scm.ScmConstraintViolationException;
import sonia.scm.event.ScmEventBus;
import sonia.scm.lifecycle.Restarter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
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.singletonList;
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.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
@@ -83,6 +87,12 @@ class DefaultPluginManagerTest {
@Mock
private Restarter restarter;
@Mock
private ScmEventBus eventBus;
@Captor
private ArgumentCaptor<PluginEvent> eventCaptor;
@InjectMocks
private DefaultPluginManager manager;
@@ -537,8 +547,36 @@ class DefaultPluginManagerTest {
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
class WithoutReadPermissions {

View File

@@ -30,6 +30,7 @@ import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient;
import java.io.IOException;
@@ -37,6 +38,8 @@ import java.util.Collections;
import java.util.Set;
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;
@ExtendWith(MockitoExtension.class)
@@ -50,6 +53,9 @@ class PluginCenterLoaderTest {
@Mock
private PluginCenterDtoMapper mapper;
@Mock
private ScmEventBus eventBus;
@InjectMocks
private PluginCenterLoader loader;
@@ -71,4 +77,13 @@ class PluginCenterLoaderTest {
Set<AvailablePlugin> fetch = loader.load(PLUGIN_URL);
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