Merge with 2.0.0-m3

This commit is contained in:
Rene Pfeuffer
2019-11-25 14:35:47 +01:00
39 changed files with 499 additions and 197 deletions

View File

@@ -59,7 +59,7 @@ public final class SCMContext
*/
public static final User ANONYMOUS = new User(USER_ANONYMOUS,
"SCM Anonymous",
"scm-anonymous@scm-manager.com");
"scm-anonymous@scm-manager.org");
/** Singleton instance of {@link SCMContextProvider} */
private static volatile SCMContextProvider provider;

View File

@@ -25,7 +25,7 @@ public class AvailablePlugin implements Plugin {
return pending;
}
public AvailablePlugin install() {
AvailablePlugin install() {
Preconditions.checkState(!pending, "installation is already pending");
return new AvailablePlugin(pluginDescriptor, true);
}

View File

@@ -113,7 +113,6 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler
Repository repository = new Repository();
repository.setName(repositoryName);
repository.setPublicReadable(false);
repository.setType(getTypeName());
return repository;

View File

@@ -83,8 +83,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private String name;
@XmlElement(name = "permission")
private Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private String type;
@@ -225,15 +223,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return Util.isEmpty(healthCheckFailures);
}
/**
* Returns true if the {@link Repository} is public readable.
*
* @return true if the {@link Repository} is public readable
*/
public boolean isPublicReadable() {
return publicReadable;
}
/**
* Returns true if the {@link Repository} is valid.
* <ul>
@@ -292,10 +281,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return this.permissions.remove(permission);
}
public void setPublicReadable(boolean publicReadable) {
this.publicReadable = publicReadable;
}
public void setType(String type) {
this.type = type;
}
@@ -332,7 +317,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
repository.setLastModified(lastModified);
repository.setDescription(description);
repository.setPermissions(permissions);
repository.setPublicReadable(publicReadable);
// do not copy health check results
}
@@ -360,7 +344,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
&& Objects.equal(name, other.name)
&& Objects.equal(contact, other.contact)
&& Objects.equal(description, other.description)
&& Objects.equal(publicReadable, other.publicReadable)
&& Objects.equal(permissions, other.permissions)
&& Objects.equal(type, other.type)
&& Objects.equal(creationDate, other.creationDate)
@@ -371,7 +354,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
@Override
public int hashCode() {
return Objects.hashCode(id, namespace, name, contact, description, publicReadable,
return Objects.hashCode(id, namespace, name, contact, description,
permissions, type, creationDate, lastModified, properties,
healthCheckFailures);
}
@@ -384,7 +367,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
.add("name", name)
.add("contact", contact)
.add("description", description)
.add("publicReadable", publicReadable)
.add("permissions", permissions)
.add("type", type)
.add("lastModified", lastModified)

View File

@@ -1,6 +1,7 @@
package sonia.scm.it;
import org.apache.http.HttpStatus;
import org.assertj.core.api.AbstractCharSequenceAssert;
import org.assertj.core.util.Lists;
import org.junit.Before;
import org.junit.Ignore;
@@ -28,6 +29,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
@@ -94,8 +96,7 @@ public class DiffITCase {
String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff)
.isEqualTo(expected);
assertDiffsAreEqual(svnDiff, expected);
}
@Test
@@ -107,8 +108,7 @@ public class DiffITCase {
String gitDiff = getDiff(RepositoryUtil.removeAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff)
.isEqualTo(expected);
assertDiffsAreEqual(svnDiff, expected);
}
@Test
@@ -120,8 +120,7 @@ public class DiffITCase {
String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff)
.isEqualTo(expected);
assertDiffsAreEqual(svnDiff, expected);
}
@Test
@@ -161,21 +160,17 @@ public class DiffITCase {
String fileContent = getFileContent("/diff/largefile/original/SvnDiffGenerator_forTest");
String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff)
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
fileContent = getFileContent("/diff/largefile/modified/v1/SvnDiffGenerator_forTest");
svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff)
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
fileContent = getFileContent("/diff/largefile/modified/v2/SvnDiffGenerator_forTest");
svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff)
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
}
/**
@@ -196,8 +191,7 @@ public class DiffITCase {
Changeset commit1 = RepositoryUtil.addFileAndCommit(gitRepositoryClient, fileName, ADMIN_USERNAME, "");
String svnDiff = getDiff(commit, svnRepositoryResponse);
String gitDiff = getDiff(commit1, gitRepositoryResponse);
assertThat(svnDiff)
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
}
@@ -218,8 +212,7 @@ public class DiffITCase {
String gitDiff = getDiff(RepositoryUtil.addFileAndCommit(gitRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff)
.isEqualTo(expected);
assertDiffsAreEqual(svnDiff, expected);
}
public String getFileContent(String name) throws URISyntaxException, IOException {
@@ -242,6 +235,12 @@ public class DiffITCase {
return gitDiff.replaceAll(".*(index.*\n)", "");
}
private void assertDiffsAreEqual(String svnDiff, String gitDiff) {
assertThat(svnDiff)
.as("diffs are different\n\nsvn:\n==================================================\n\n%s\n\ngit:\n==================================================\n\n%s)", svnDiff, gitDiff)
.isEqualTo(gitDiff);
}
private String getDiff(Changeset svnChangeset, ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> svnRepositoryResponse) {
return svnRepositoryResponse.requestChangesets()
.requestDiffInGitFormat(svnChangeset.getId())

View File

@@ -110,6 +110,7 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
return;
}
numberOfPotentialBeginning = -1;
inPotentialQuotedLine = false;
}
if (inPotentialQuotedLine && i == '"') {

View File

@@ -16,7 +16,7 @@ public class GitDiffCommand_DequoteOutputStreamTest {
"--- /dev/null\n" +
"+++ \"b/\\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 \\303\\245g\\303\\260f\\303\\237\"\n" +
"@@ -0,0 +1 @@\n" +
"+rthms";
"+String s = \"quotes shall be kept\";";
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
GitDiffCommand.DequoteOutputStream stream = new GitDiffCommand.DequoteOutputStream(buffer);
@@ -30,6 +30,6 @@ public class GitDiffCommand_DequoteOutputStreamTest {
"--- /dev/null\n" +
"+++ b/úüþëéåëåé ågðfß\n" +
"@@ -0,0 +1 @@\n" +
"+rthms");
"+String s = \"quotes shall be kept\";");
}
}

View File

@@ -1,6 +1,7 @@
import React from "react";
import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types";
import Autocomplete from "./Autocomplete";
import { apiClient } from "./apiclient";
export type AutocompleteProps = {
autocompleteLink?: string;
@@ -19,7 +20,8 @@ export default class UserGroupAutocomplete extends React.Component<Props> {
loadSuggestions = (inputValue: string): Promise<SelectValue[]> => {
const url = this.props.autocompleteLink;
const link = url + "?q=";
return fetch(link + inputValue)
return apiClient
.get(link + inputValue)
.then(response => response.json())
.then((json: AutocompleteObject[]) => {
return json.map(element => {

View File

@@ -27,13 +27,17 @@ const extractXsrfToken = () => {
};
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
const headers: { [key: string]: string } = {
Cache: "no-cache",
if (!o.headers) {
o.headers = {};
}
// @ts-ignore We are sure that here we only get headers of type Record<string, string>
const headers: Record<string, string> = o.headers;
headers["Cache"] = "no-cache";
// identify the request as ajax request
"X-Requested-With": "XMLHttpRequest",
headers["X-Requested-With"] = "XMLHttpRequest";
// identify the web interface
"X-SCM-Client": "WUI"
};
headers["X-SCM-Client"] = "WUI";
const xsrf = extractXsrfToken();
if (xsrf) {
@@ -80,23 +84,32 @@ class ApiClient {
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
}
post(url: string, payload?: any, contentType = "application/json") {
return this.httpRequestWithJSONBody("POST", url, contentType, payload);
post(url: string, payload?: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload);
}
postBinary(url: string, fileAppender: (p: FormData) => void) {
postText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithTextBody("POST", url, additionalHeaders, payload);
}
putText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithTextBody("PUT", url, additionalHeaders, payload);
}
postBinary(url: string, fileAppender: (p: FormData) => void, additionalHeaders: Record<string, string> = {}) {
const formData = new FormData();
fileAppender(formData);
const options: RequestInit = {
method: "POST",
body: formData
body: formData,
headers: additionalHeaders
};
return this.httpRequestWithBinaryBody(options, url);
}
put(url: string, payload: any, contentType = "application/json") {
return this.httpRequestWithJSONBody("PUT", url, contentType, payload);
put(url: string, payload: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload);
}
head(url: string) {
@@ -115,9 +128,16 @@ class ApiClient {
return fetch(createUrl(url), options).then(handleFailure);
}
httpRequestWithJSONBody(method: string, url: string, contentType: string, payload?: any): Promise<Response> {
httpRequestWithJSONBody(
method: string,
url: string,
contentType: string,
additionalHeaders: Record<string, string>,
payload?: any
): Promise<Response> {
const options: RequestInit = {
method: method
method: method,
headers: additionalHeaders
};
if (payload) {
options.body = JSON.stringify(payload);
@@ -125,13 +145,27 @@ class ApiClient {
return this.httpRequestWithBinaryBody(options, url, contentType);
}
httpRequestWithTextBody(
method: string,
url: string,
additionalHeaders: Record<string, string> = {},
payload: string
) {
const options: RequestInit = {
method: method,
headers: additionalHeaders
};
options.body = payload;
return this.httpRequestWithBinaryBody(options, url, "text/plain");
}
httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) {
options = applyFetchOptions(options);
if (contentType) {
if (!options.headers) {
options.headers = new Headers();
options.headers = {};
}
// @ts-ignore
// @ts-ignore We are sure that here we only get headers of type Record<string, string>
options.headers["Content-Type"] = contentType;
}

View File

@@ -1,11 +1,14 @@
import React from "react";
import DiffFile from "./DiffFile";
import { DiffObjectProps, File } from "./DiffTypes";
import Notification from "../Notification";
import { WithTranslation, withTranslation } from "react-i18next";
type Props = DiffObjectProps & {
type Props = WithTranslation &
DiffObjectProps & {
diff: File[];
defaultCollapse?: boolean;
};
};
class Diff extends React.Component<Props> {
static defaultProps: Partial<Props> = {
@@ -13,15 +16,17 @@ class Diff extends React.Component<Props> {
};
render() {
const { diff, ...fileProps } = this.props;
const { diff, t, ...fileProps } = this.props;
return (
<>
{diff.map((file, index) => (
<DiffFile key={index} file={file} {...fileProps} {...this.props} />
))}
{diff.length === 0 ? (
<Notification type="info">{t("diff.noDiffFound")}</Notification>
) : (
diff.map((file, index) => <DiffFile key={index} file={file} {...fileProps} {...this.props} />)
)}
</>
);
}
}
export default Diff;
export default withTranslation("repos")(Diff);

View File

@@ -43,6 +43,7 @@ class LoadingDiff extends React.Component<Props, State> {
fetchDiff = () => {
const { url } = this.props;
this.setState({loading: true});
apiClient
.get(url)
.then(response => response.text())

View File

@@ -7,41 +7,6 @@ const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const root = path.resolve(process.cwd(), "scm-ui");
module.exports = [
{
context: root,
entry: "./ui-styles/src/scm.scss",
module: {
rules: [
{
test: /\.(css|scss|sass)$/i,
use: [
{
loader: MiniCssExtractPlugin.loader
},
"css-loader",
"sass-loader"
]
},
{
test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/,
use: ["file-loader"]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "ui-styles.css",
ignoreOrder: false
})
],
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
},
output: {
path: path.join(root, "target", "assets"),
filename: "ui-styles.bundle.js"
}
},
{
context: root,
entry: {
@@ -142,6 +107,41 @@ module.exports = [
}
}
},
{
context: root,
entry: "./ui-styles/src/scm.scss",
module: {
rules: [
{
test: /\.(css|scss|sass)$/i,
use: [
{
loader: MiniCssExtractPlugin.loader
},
"css-loader",
"sass-loader"
]
},
{
test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/,
use: ["file-loader"]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "ui-styles.css",
ignoreOrder: false
})
],
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
},
output: {
path: path.join(root, "target", "assets"),
filename: "ui-styles.bundle.js"
}
},
{
context: path.resolve(root),
entry: {

View File

@@ -809,10 +809,6 @@ form .field:not(.is-grouped) {
}
}
.modal-card-body div div:last-child {
border-bottom: none;
}
// cursor
.has-cursor-pointer {
cursor: pointer;

View File

@@ -177,7 +177,8 @@
},
"diff": {
"sideBySide": "Zweispaltig",
"combined": "Kombiniert"
"combined": "Kombiniert",
"noDiffFound": "Kein Diff zwischen den ausgewählten Branches gefunden."
},
"fileUpload": {
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",

View File

@@ -184,7 +184,8 @@
"copy": "copied"
},
"sideBySide": "side-by-side",
"combined": "combined"
"combined": "combined",
"noDiffFound": "No Diff between the selected branches found."
},
"fileUpload": {
"clickHere": "Click here to select your file",

View File

@@ -184,7 +184,8 @@
"copy": "copiado"
},
"sideBySide": "dos columnas",
"combined": "combinado"
"combined": "combinado",
"noDiffFound": "No se encontraron diferencias entre las ramas seleccionadas."
},
"fileUpload": {
"clickHere": "Haga click aquí para seleccionar su fichero",

View File

@@ -1,7 +1,7 @@
import { apiClient } from "@scm-manager/ui-components";
const waitForRestart = () => {
const endTime = Number(new Date()) + 10000;
const endTime = Number(new Date()) + 60000;
let started = false;
const executor = (resolve, reject) => {

View File

@@ -7,6 +7,7 @@ import { Page } from "@scm-manager/ui-components";
import { getGroupsLink, getUserAutoCompleteLink } from "../../modules/indexResource";
import { createGroup, isCreateGroupPending, getCreateGroupFailure, createGroupReset } from "../modules/groups";
import GroupForm from "../components/GroupForm";
import { apiClient } from "@scm-manager/ui-components/src";
type Props = WithTranslation & {
createGroup: (link: string, group: Group, callback?: () => void) => void;
@@ -40,7 +41,8 @@ class CreateGroup extends React.Component<Props> {
loadUserAutocompletion = (inputValue: string) => {
const url = this.props.autocompleteLink + "?q=";
return fetch(url + inputValue)
return apiClient
.get(url + inputValue)
.then(response => response.json())
.then(json => {
return json.map(element => {

View File

@@ -8,6 +8,7 @@ import { Group } from "@scm-manager/ui-types";
import { ErrorNotification } from "@scm-manager/ui-components";
import { getUserAutoCompleteLink } from "../../modules/indexResource";
import DeleteGroup from "./DeleteGroup";
import { apiClient } from "@scm-manager/ui-components/src";
type Props = {
group: Group;
@@ -36,7 +37,8 @@ class EditGroup extends React.Component<Props> {
loadUserAutocompletion = (inputValue: string) => {
const url = this.props.autocompleteLink + "?q=";
return fetch(url + inputValue)
return apiClient
.get(url + inputValue)
.then(response => response.json())
.then(json => {
return json.map(element => {

View File

@@ -49,13 +49,22 @@ class Groups extends React.Component<Props> {
componentDidUpdate = (prevProps: Props) => {
const { loading, list, page, groupLink, location, fetchGroupsByPage } = this.props;
if (list && page && !loading) {
const statePage: number = list.page + 1;
const statePage: number = this.resolveStatePage();
if (page !== statePage || prevProps.location.search !== location.search) {
fetchGroupsByPage(groupLink, page, urls.getQueryStringFromLocation(location));
}
}
};
resolveStatePage = () => {
const { list } = this.props;
if (list.page) {
return list.page + 1;
}
// set page to 1 if undefined, because if groups couldn't be fetched it would lead to an fetch-loop otherwise
return 1;
};
render() {
const { groups, loading, error, canAddGroups, t } = this.props;
return (

View File

@@ -114,7 +114,7 @@ const mapDispatchToProps = dispatch => {
const mapStateToProps = (state, ownProps) => {
const { repository } = ownProps;
const loading = isFetchBranchesPending(state, repository) || isCreateBranchPending(state, repository);
const error = getFetchBranchesFailure(state, repository) || getCreateBranchFailure(state);
const error = getFetchBranchesFailure(state, repository) || getCreateBranchFailure(state, repository);
const branches = getBranches(state, repository);
const createBranchesLink = getBranchCreateLink(state, repository);
return {

View File

@@ -482,14 +482,14 @@ describe("branches", () => {
it("should return error when create branch did fail", () => {
const state = {
failure: {
[CREATE_BRANCH]: error
[CREATE_BRANCH + `/${repository.namespace}/${repository.name}`]: error
}
};
expect(getCreateBranchFailure(state)).toEqual(error);
expect(getCreateBranchFailure(state, repository)).toEqual(error);
});
it("should return undefined when create branch did not fail", () => {
expect(getCreateBranchFailure({})).toBe(undefined);
expect(getCreateBranchFailure({}, repository)).toBe(undefined);
});
});
});

View File

@@ -186,8 +186,8 @@ export function isCreateBranchPending(state: object, repository: Repository) {
return isPending(state, CREATE_BRANCH, createKey(repository));
}
export function getCreateBranchFailure(state: object) {
return getFailure(state, CREATE_BRANCH);
export function getCreateBranchFailure(state: object, repository: Repository) {
return getFailure(state, CREATE_BRANCH, createKey(repository));
}
export function createBranchPending(repository: Repository): Action {

View File

@@ -49,13 +49,22 @@ class Users extends React.Component<Props> {
componentDidUpdate = (prevProps: Props) => {
const { loading, list, page, usersLink, location, fetchUsersByPage } = this.props;
if (list && page && !loading) {
const statePage: number = list.page + 1;
const statePage: number = this.resolveStatePage();
if (page !== statePage || prevProps.location.search !== location.search) {
fetchUsersByPage(usersLink, page, urls.getQueryStringFromLocation(location));
}
}
};
resolveStatePage = (props = this.props) => {
const { list } = props;
if (list.page) {
return list.page + 1;
}
// set page to 1 if undefined, because if users couldn't be fetched it would lead to a fetch-loop otherwise
return 1;
};
render() {
const { users, loading, error, canAddUsers, t } = this.props;
return (

View File

@@ -8,7 +8,6 @@ public abstract class RepositoryDtoToRepositoryMapper extends BaseDtoMapper {
@Mapping(target = "creationDate", ignore = true)
@Mapping(target = "id", ignore = true)
@Mapping(target = "publicReadable", ignore = true)
@Mapping(target = "healthCheckFailures", ignore = true)
public abstract Repository map(RepositoryDto repositoryDto, @Context String id);

View File

@@ -61,7 +61,7 @@ public class SetupContextListener implements ServletContextListener {
@Override
public void run() {
if (isFirstStart()) {
if (shouldCreateAdminAccount()) {
createAdminAccount();
}
if (anonymousUserRequiredButNotExists()) {
@@ -73,8 +73,12 @@ public class SetupContextListener implements ServletContextListener {
return scmConfiguration.isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS);
}
private boolean isFirstStart() {
return userManager.getAll().isEmpty();
private boolean shouldCreateAdminAccount() {
return userManager.getAll().isEmpty() || onlyAnonymousUserExists();
}
private boolean onlyAnonymousUserExists() {
return userManager.getAll().size() == 1 && userManager.contains(SCMContext.USER_ANONYMOUS);
}
private void createAdminAccount() {

View File

@@ -187,7 +187,7 @@ public class DefaultPluginManager implements PluginManager {
if (!pendingInstallations.isEmpty()) {
if (restartAfterInstallation) {
restart("plugin installation");
triggerRestart("plugin installation");
} else {
pendingInstallQueue.addAll(pendingInstallations);
updateMayUninstallFlag();
@@ -205,7 +205,7 @@ public class DefaultPluginManager implements PluginManager {
markForUninstall(installed);
if (restartAfterInstallation) {
restart("plugin installation");
triggerRestart("plugin installation");
} else {
updateMayUninstallFlag();
}
@@ -238,12 +238,19 @@ public class DefaultPluginManager implements PluginManager {
public void executePendingAndRestart() {
PluginPermissions.manage().check();
if (!pendingInstallQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
restart("execute pending plugin changes");
triggerRestart("execute pending plugin changes");
}
}
private void restart(String cause) {
@VisibleForTesting
void triggerRestart(String cause) {
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
eventBus.post(new RestartEvent(PluginManager.class, cause));
}).start();
}
private void cancelPending(List<PendingPluginInstallation> pendingInstallations) {

View File

@@ -168,8 +168,7 @@ public class AuthorizationChangedEventProducer {
}
private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) {
return repository.isPublicReadable() != beforeModification.isPublicReadable()
|| !(repository.getPermissions().containsAll(beforeModification.getPermissions()) && beforeModification.getPermissions().containsAll(repository.getPermissions()));
return !(repository.getPermissions().containsAll(beforeModification.getPermissions()) && beforeModification.getPermissions().containsAll(repository.getPermissions()));
}
private void fireEventForEveryUser() {

View File

@@ -90,7 +90,6 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep {
repository.setCreationDate(oldRepository.creationDate);
repository.setHealthCheckFailures(oldRepository.healthCheckFailures);
repository.setLastModified(oldRepository.lastModified);
repository.setPublicReadable(oldRepository.publicReadable);
return repository;
}
@@ -149,8 +148,6 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep {
private String name;
@XmlElement(name = "permission")
private final Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false;
private String type;
}

View File

@@ -0,0 +1,96 @@
package sonia.scm.update.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.user.User;
import sonia.scm.user.xml.XmlUserDAO;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBException;
import static sonia.scm.version.Version.parse;
@Extension
public class PublicFlagUpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(PublicFlagUpdateStep.class);
private static final String V1_REPOSITORY_BACKUP_FILENAME = "repositories.xml.v1.backup";
private final SCMContextProvider contextProvider;
private final XmlUserDAO userDAO;
private final XmlRepositoryDAO repositoryDAO;
@Inject
public PublicFlagUpdateStep(SCMContextProvider contextProvider, XmlUserDAO userDAO, XmlRepositoryDAO repositoryDAO) {
this.contextProvider = contextProvider;
this.userDAO = userDAO;
this.repositoryDAO = repositoryDAO;
}
@Override
public void doUpdate() throws JAXBException {
LOG.info("Migrating public flags of repositories as RepositoryRolePermission 'READ' for user '_anonymous'");
V1RepositoryHelper.readV1Database(contextProvider, V1_REPOSITORY_BACKUP_FILENAME).ifPresent(
v1RepositoryDatabase -> {
createNewAnonymousUserIfNotExists();
deleteOldAnonymousUserIfAvailable();
addRepositoryReadPermissionForAnonymousUser(v1RepositoryDatabase);
}
);
}
@Override
public Version getTargetVersion() {
return parse("2.0.3");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.repository.xml";
}
private void addRepositoryReadPermissionForAnonymousUser(V1RepositoryHelper.V1RepositoryDatabase v1RepositoryDatabase) {
User v2AnonymousUser = userDAO.get(SCMContext.USER_ANONYMOUS);
v1RepositoryDatabase.repositoryList.repositories
.stream()
.filter(V1Repository::isPublic)
.forEach(v1Repository -> {
Repository v2Repository = repositoryDAO.get(v1Repository.getId());
LOG.info(String.format("Add RepositoryRole 'READ' to _anonymous user for repository: %s - %s/%s", v2Repository.getId(), v2Repository.getNamespace(), v2Repository.getName()));
v2Repository.addPermission(new RepositoryPermission(v2AnonymousUser.getId(), "READ", false));
repositoryDAO.modify(v2Repository);
});
}
private void createNewAnonymousUserIfNotExists() {
if (!userExists(SCMContext.USER_ANONYMOUS)) {
LOG.info("Create new _anonymous user");
userDAO.add(SCMContext.ANONYMOUS);
}
}
private void deleteOldAnonymousUserIfAvailable() {
String oldAnonymous = "anonymous";
if (userExists(oldAnonymous)) {
User anonymousUser = userDAO.get(oldAnonymous);
LOG.info("Delete obsolete anonymous user");
userDAO.delete(anonymousUser);
}
}
private boolean userExists(String username) {
return userDAO
.getAll()
.stream()
.anyMatch(user -> user.getName().equals(username));
}
}

View File

@@ -4,6 +4,7 @@ import sonia.scm.update.V1Properties;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
@@ -16,6 +17,7 @@ public class V1Repository {
private String description;
private String id;
private String name;
@XmlElement(name="public")
private boolean isPublic;
private boolean archived;
private String type;

View File

@@ -0,0 +1,57 @@
package sonia.scm.update.repository;
import sonia.scm.SCMContextProvider;
import sonia.scm.store.StoreConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
class V1RepositoryHelper {
static Optional<File> resolveV1File(SCMContextProvider contextProvider, String filename) {
File v1XmlFile = contextProvider.resolve(Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve(filename)).toFile();
if (v1XmlFile.exists()) {
return Optional.of(v1XmlFile);
}
return Optional.empty();
}
static Optional<V1RepositoryDatabase> readV1Database(SCMContextProvider contextProvider, String filename) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
Optional<File> file = resolveV1File(contextProvider, filename);
if (file.isPresent()) {
Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(file.get());
if (unmarshal instanceof V1RepositoryDatabase) {
return of((V1RepositoryDatabase) unmarshal);
} else {
return empty();
}
}
return empty();
}
static class RepositoryList {
@XmlElement(name = "repository")
List<V1Repository> repositories;
}
@XmlRootElement(name = "repository-db")
@XmlAccessorType(XmlAccessType.FIELD)
static class V1RepositoryDatabase {
long creationTime;
Long lastModified;
@XmlElement(name = "repositories")
RepositoryList repositoryList;
}
}

View File

@@ -17,26 +17,18 @@ import sonia.scm.update.V1Properties;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static sonia.scm.update.V1PropertyReader.REPOSITORY_PROPERTY_READER;
import static sonia.scm.update.repository.V1RepositoryHelper.resolveV1File;
import static sonia.scm.version.Version.parse;
/**
@@ -59,6 +51,8 @@ import static sonia.scm.version.Version.parse;
@Extension
public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
private final String V1_REPOSITORY_FILENAME = "repositories" + StoreConstants.FILE_EXTENSION;
private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class);
private final SCMContextProvider contextProvider;
@@ -97,12 +91,11 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
@Override
public void doUpdate() throws JAXBException {
if (!resolveV1File().exists()) {
if (!resolveV1File(contextProvider, V1_REPOSITORY_FILENAME).isPresent()) {
LOG.info("no v1 repositories database file found");
return;
}
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class);
readV1Database(jaxbContext).ifPresent(
V1RepositoryHelper.readV1Database(contextProvider, V1_REPOSITORY_FILENAME).ifPresent(
v1Database -> {
v1Database.repositoryList.repositories.forEach(this::readMigrationEntry);
v1Database.repositoryList.repositories.forEach(this::update);
@@ -112,13 +105,12 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
}
public List<V1Repository> getRepositoriesWithoutMigrationStrategies() {
if (!resolveV1File().exists()) {
if (!resolveV1File(contextProvider, V1_REPOSITORY_FILENAME).isPresent()) {
LOG.info("no v1 repositories database file found");
return emptyList();
}
try {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlRepositoryV1UpdateStep.V1RepositoryDatabase.class);
return readV1Database(jaxbContext)
return V1RepositoryHelper.readV1Database(contextProvider, V1_REPOSITORY_FILENAME)
.map(v1Database -> v1Database.repositoryList.repositories.stream())
.orElse(Stream.empty())
.filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent())
@@ -196,33 +188,4 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission());
}
private Optional<V1RepositoryDatabase> readV1Database(JAXBContext jaxbContext) throws JAXBException {
Object unmarshal = jaxbContext.createUnmarshaller().unmarshal(resolveV1File());
if (unmarshal instanceof V1RepositoryDatabase) {
return of((V1RepositoryDatabase) unmarshal);
} else {
return empty();
}
}
private File resolveV1File() {
return contextProvider
.resolve(
Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME).resolve("repositories" + StoreConstants.FILE_EXTENSION)
).toFile();
}
private static class RepositoryList {
@XmlElement(name = "repository")
private List<V1Repository> repositories;
}
@XmlRootElement(name = "repository-db")
@XmlAccessorType(XmlAccessType.FIELD)
private static class V1RepositoryDatabase {
private long creationTime;
private Long lastModified;
@XmlElement(name = "repositories")
private RepositoryList repositoryList;
}
}

View File

@@ -94,7 +94,6 @@ public class RepositorySimplePermissionITCase
repository.setName("test-repo");
repository.setType("git");
// repository.setPublicReadable(false);
ScmClient client = createAdminClient();

View File

@@ -70,7 +70,19 @@ class SetupContextListenerTest {
}
@Test
void shouldCreateAdminAccountAndAssignPermissions() {
void shouldCreateAdminAccountIfNoUserExistsAndAssignPermissions() {
when(passwordService.encryptPassword("scmadmin")).thenReturn("secret");
setupContextListener.contextInitialized(null);
verifyAdminCreated();
verifyAdminPermissionsAssigned();
}
@Test
void shouldCreateAdminAccountIfOnlyAnonymousUserExistsAndAssignPermissions() {
when(userManager.getAll()).thenReturn(Lists.newArrayList(SCMContext.ANONYMOUS));
when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(true);
when(passwordService.encryptPassword("scmadmin")).thenReturn("secret");
setupContextListener.contextInitialized(null);

View File

@@ -12,13 +12,10 @@ 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.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.RestartEvent;
import java.io.IOException;
import java.nio.file.Files;
@@ -48,9 +45,6 @@ import static sonia.scm.plugin.PluginTestHelper.createInstalled;
@ExtendWith(TempDirectory.class)
class DefaultPluginManagerTest {
@Mock
private ScmEventBus eventBus;
@Mock
private PluginLoader loader;
@@ -60,12 +54,13 @@ class DefaultPluginManagerTest {
@Mock
private PluginInstaller installer;
@InjectMocks
private DefaultPluginManager manager;
@Mock
private Subject subject;
private boolean restartTriggered = false;
@BeforeEach
void mockInstaller() {
lenient().when(installer.install(any())).then(ic -> {
@@ -74,6 +69,16 @@ class DefaultPluginManagerTest {
});
}
@BeforeEach
void createPluginManagerToTestWithCapturedRestart() {
manager = new DefaultPluginManager(null, loader, center, installer) { // event bus is only used in restart and this is replaced here
@Override
void triggerRestart(String cause) {
restartTriggered = true;
}
};
}
@Nested
class WithAdminPermissions {
@@ -180,7 +185,7 @@ class DefaultPluginManagerTest {
manager.install("scm-git-plugin", false);
verify(installer).install(git);
verify(eventBus, never()).post(any());
assertThat(restartTriggered).isFalse();
}
@Test
@@ -258,7 +263,7 @@ class DefaultPluginManagerTest {
manager.install("scm-git-plugin", true);
verify(installer).install(git);
verify(eventBus).post(any(RestartEvent.class));
assertThat(restartTriggered).isTrue();
}
@Test
@@ -267,7 +272,7 @@ class DefaultPluginManagerTest {
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(gitInstalled));
manager.install("scm-git-plugin", true);
verify(eventBus, never()).post(any());
assertThat(restartTriggered).isFalse();
}
@Test
@@ -289,14 +294,14 @@ class DefaultPluginManagerTest {
manager.install("scm-review-plugin", false);
manager.executePendingAndRestart();
verify(eventBus).post(any(RestartEvent.class));
assertThat(restartTriggered).isTrue();
}
@Test
void shouldNotSendRestartEventWithoutPendingPlugins() {
manager.executePendingAndRestart();
verify(eventBus, never()).post(any());
assertThat(restartTriggered).isFalse();
}
@Test
@@ -447,7 +452,7 @@ class DefaultPluginManagerTest {
manager.executePendingAndRestart();
verify(eventBus).post(any(RestartEvent.class));
assertThat(restartTriggered).isTrue();
}
@Test

View File

@@ -0,0 +1,118 @@
package sonia.scm.update.repository;
import org.junit.jupiter.api.BeforeEach;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContext;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryRolePermissions;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.update.UpdateStepTestUtil;
import sonia.scm.user.User;
import sonia.scm.user.xml.XmlUserDAO;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junitpioneer.jupiter.TempDirectory.TempDir;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ExtendWith(TempDirectory.class)
class PublicFlagUpdateStepTest {
@Mock
XmlUserDAO userDAO;
@Mock
XmlRepositoryDAO repositoryDAO;
@Captor
ArgumentCaptor<Repository> repositoryCaptor;
private UpdateStepTestUtil testUtil;
private PublicFlagUpdateStep updateStep;
private Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
@BeforeEach
void mockScmHome(@TempDir Path tempDir) throws IOException {
testUtil = new UpdateStepTestUtil(tempDir);
updateStep = new PublicFlagUpdateStep(testUtil.getContextProvider(), userDAO, repositoryDAO);
//prepare backup xml
V1RepositoryFileSystem.createV1Home(tempDir);
Files.move(tempDir.resolve("config").resolve("repositories.xml"), tempDir.resolve("config").resolve("repositories.xml.v1.backup"));
when(repositoryDAO.get((String) any())).thenReturn(REPOSITORY);
}
@Test
void shouldDeleteOldAnonymousUserIfExists() throws JAXBException {
User anonymous = new User("anonymous");
when(userDAO.getAll()).thenReturn(Collections.singleton(anonymous));
doReturn(anonymous).when(userDAO).get("anonymous");
doReturn(SCMContext.ANONYMOUS).when(userDAO).get(SCMContext.USER_ANONYMOUS);
updateStep.doUpdate();
verify(userDAO).delete(anonymous);
}
@Test
void shouldNotTryToDeleteOldAnonymousUserIfNotExists() throws JAXBException {
when(userDAO.getAll()).thenReturn(Collections.emptyList());
doReturn(SCMContext.ANONYMOUS).when(userDAO).get(SCMContext.USER_ANONYMOUS);
updateStep.doUpdate();
verify(userDAO, never()).delete(any());
}
@Test
void shouldCreateNewAnonymousUserIfNotExists() throws JAXBException {
doReturn(SCMContext.ANONYMOUS).when(userDAO).get(SCMContext.USER_ANONYMOUS);
when(userDAO.getAll()).thenReturn(Collections.singleton(new User("trillian")));
updateStep.doUpdate();
verify(userDAO).add(SCMContext.ANONYMOUS);
}
@Test
void shouldNotCreateNewAnonymousUserIfAlreadyExists() throws JAXBException {
doReturn(SCMContext.ANONYMOUS).when(userDAO).get(SCMContext.USER_ANONYMOUS);
when(userDAO.getAll()).thenReturn(Collections.singleton(new User("_anonymous")));
updateStep.doUpdate();
verify(userDAO, never()).add(SCMContext.ANONYMOUS);
}
@Test
void shouldMigratePublicFlagToAnonymousRepositoryPermission() throws JAXBException {
when(userDAO.getAll()).thenReturn(Collections.emptyList());
when(userDAO.get("_anonymous")).thenReturn(SCMContext.ANONYMOUS);
updateStep.doUpdate();
verify(repositoryDAO, times(2)).modify(repositoryCaptor.capture());
RepositoryPermission migratedRepositoryPermission = repositoryCaptor.getValue().getPermissions().iterator().next();
assertThat(migratedRepositoryPermission.getName()).isEqualTo(SCMContext.USER_ANONYMOUS);
assertThat(migratedRepositoryPermission.getRole()).isEqualTo("READ");
assertThat(migratedRepositoryPermission.isGroupPermission()).isFalse();
}
}