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, public static final User ANONYMOUS = new User(USER_ANONYMOUS,
"SCM Anonymous", "SCM Anonymous",
"scm-anonymous@scm-manager.com"); "scm-anonymous@scm-manager.org");
/** Singleton instance of {@link SCMContextProvider} */ /** Singleton instance of {@link SCMContextProvider} */
private static volatile SCMContextProvider provider; private static volatile SCMContextProvider provider;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ public class GitDiffCommand_DequoteOutputStreamTest {
"--- /dev/null\n" + "--- /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" + "+++ \"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" + "@@ -0,0 +1 @@\n" +
"+rthms"; "+String s = \"quotes shall be kept\";";
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); ByteArrayOutputStream buffer = new ByteArrayOutputStream();
GitDiffCommand.DequoteOutputStream stream = new GitDiffCommand.DequoteOutputStream(buffer); GitDiffCommand.DequoteOutputStream stream = new GitDiffCommand.DequoteOutputStream(buffer);
@@ -30,6 +30,6 @@ public class GitDiffCommand_DequoteOutputStreamTest {
"--- /dev/null\n" + "--- /dev/null\n" +
"+++ b/úüþëéåëåé ågðfß\n" + "+++ b/úüþëéåëåé ågðfß\n" +
"@@ -0,0 +1 @@\n" + "@@ -0,0 +1 @@\n" +
"+rthms"); "+String s = \"quotes shall be kept\";");
} }
} }

View File

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

View File

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

View File

@@ -1,11 +1,14 @@
import React from "react"; import React from "react";
import DiffFile from "./DiffFile"; import DiffFile from "./DiffFile";
import { DiffObjectProps, File } from "./DiffTypes"; import { DiffObjectProps, File } from "./DiffTypes";
import Notification from "../Notification";
import { WithTranslation, withTranslation } from "react-i18next";
type Props = DiffObjectProps & { type Props = WithTranslation &
diff: File[]; DiffObjectProps & {
defaultCollapse?: boolean; diff: File[];
}; defaultCollapse?: boolean;
};
class Diff extends React.Component<Props> { class Diff extends React.Component<Props> {
static defaultProps: Partial<Props> = { static defaultProps: Partial<Props> = {
@@ -13,15 +16,17 @@ class Diff extends React.Component<Props> {
}; };
render() { render() {
const { diff, ...fileProps } = this.props; const { diff, t, ...fileProps } = this.props;
return ( return (
<> <>
{diff.map((file, index) => ( {diff.length === 0 ? (
<DiffFile key={index} file={file} {...fileProps} {...this.props} /> <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 = () => { fetchDiff = () => {
const { url } = this.props; const { url } = this.props;
this.setState({loading: true});
apiClient apiClient
.get(url) .get(url)
.then(response => response.text()) .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"); const root = path.resolve(process.cwd(), "scm-ui");
module.exports = [ 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, context: root,
entry: { 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), context: path.resolve(root),
entry: { entry: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,13 +49,22 @@ class Groups extends React.Component<Props> {
componentDidUpdate = (prevProps: Props) => { componentDidUpdate = (prevProps: Props) => {
const { loading, list, page, groupLink, location, fetchGroupsByPage } = this.props; const { loading, list, page, groupLink, location, fetchGroupsByPage } = this.props;
if (list && page && !loading) { if (list && page && !loading) {
const statePage: number = list.page + 1; const statePage: number = this.resolveStatePage();
if (page !== statePage || prevProps.location.search !== location.search) { if (page !== statePage || prevProps.location.search !== location.search) {
fetchGroupsByPage(groupLink, page, urls.getQueryStringFromLocation(location)); 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() { render() {
const { groups, loading, error, canAddGroups, t } = this.props; const { groups, loading, error, canAddGroups, t } = this.props;
return ( return (

View File

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

View File

@@ -482,14 +482,14 @@ describe("branches", () => {
it("should return error when create branch did fail", () => { it("should return error when create branch did fail", () => {
const state = { const state = {
failure: { 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", () => { 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)); return isPending(state, CREATE_BRANCH, createKey(repository));
} }
export function getCreateBranchFailure(state: object) { export function getCreateBranchFailure(state: object, repository: Repository) {
return getFailure(state, CREATE_BRANCH); return getFailure(state, CREATE_BRANCH, createKey(repository));
} }
export function createBranchPending(repository: Repository): Action { export function createBranchPending(repository: Repository): Action {

View File

@@ -49,13 +49,22 @@ class Users extends React.Component<Props> {
componentDidUpdate = (prevProps: Props) => { componentDidUpdate = (prevProps: Props) => {
const { loading, list, page, usersLink, location, fetchUsersByPage } = this.props; const { loading, list, page, usersLink, location, fetchUsersByPage } = this.props;
if (list && page && !loading) { if (list && page && !loading) {
const statePage: number = list.page + 1; const statePage: number = this.resolveStatePage();
if (page !== statePage || prevProps.location.search !== location.search) { if (page !== statePage || prevProps.location.search !== location.search) {
fetchUsersByPage(usersLink, page, urls.getQueryStringFromLocation(location)); 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() { render() {
const { users, loading, error, canAddUsers, t } = this.props; const { users, loading, error, canAddUsers, t } = this.props;
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,7 +90,6 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep {
repository.setCreationDate(oldRepository.creationDate); repository.setCreationDate(oldRepository.creationDate);
repository.setHealthCheckFailures(oldRepository.healthCheckFailures); repository.setHealthCheckFailures(oldRepository.healthCheckFailures);
repository.setLastModified(oldRepository.lastModified); repository.setLastModified(oldRepository.lastModified);
repository.setPublicReadable(oldRepository.publicReadable);
return repository; return repository;
} }
@@ -149,8 +148,6 @@ public class MigrateVerbsToPermissionRoles implements UpdateStep {
private String name; private String name;
@XmlElement(name = "permission") @XmlElement(name = "permission")
private final Set<RepositoryPermission> permissions = new HashSet<>(); private final Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false; private boolean archived = false;
private String type; 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.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.util.List; import java.util.List;
@@ -16,6 +17,7 @@ public class V1Repository {
private String description; private String description;
private String id; private String id;
private String name; private String name;
@XmlElement(name="public")
private boolean isPublic; private boolean isPublic;
private boolean archived; private boolean archived;
private String type; 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 sonia.scm.version.Version;
import javax.inject.Inject; import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException; 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.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.util.Collections.emptyList; 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.V1PropertyReader.REPOSITORY_PROPERTY_READER;
import static sonia.scm.update.repository.V1RepositoryHelper.resolveV1File;
import static sonia.scm.version.Version.parse; import static sonia.scm.version.Version.parse;
/** /**
@@ -59,6 +51,8 @@ import static sonia.scm.version.Version.parse;
@Extension @Extension
public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
private final String V1_REPOSITORY_FILENAME = "repositories" + StoreConstants.FILE_EXTENSION;
private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class); private static Logger LOG = LoggerFactory.getLogger(XmlRepositoryV1UpdateStep.class);
private final SCMContextProvider contextProvider; private final SCMContextProvider contextProvider;
@@ -97,12 +91,11 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
@Override @Override
public void doUpdate() throws JAXBException { public void doUpdate() throws JAXBException {
if (!resolveV1File().exists()) { if (!resolveV1File(contextProvider, V1_REPOSITORY_FILENAME).isPresent()) {
LOG.info("no v1 repositories database file found"); LOG.info("no v1 repositories database file found");
return; return;
} }
JAXBContext jaxbContext = JAXBContext.newInstance(V1RepositoryDatabase.class); V1RepositoryHelper.readV1Database(contextProvider, V1_REPOSITORY_FILENAME).ifPresent(
readV1Database(jaxbContext).ifPresent(
v1Database -> { v1Database -> {
v1Database.repositoryList.repositories.forEach(this::readMigrationEntry); v1Database.repositoryList.repositories.forEach(this::readMigrationEntry);
v1Database.repositoryList.repositories.forEach(this::update); v1Database.repositoryList.repositories.forEach(this::update);
@@ -112,13 +105,12 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
} }
public List<V1Repository> getRepositoriesWithoutMigrationStrategies() { public List<V1Repository> getRepositoriesWithoutMigrationStrategies() {
if (!resolveV1File().exists()) { if (!resolveV1File(contextProvider, V1_REPOSITORY_FILENAME).isPresent()) {
LOG.info("no v1 repositories database file found"); LOG.info("no v1 repositories database file found");
return emptyList(); return emptyList();
} }
try { try {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlRepositoryV1UpdateStep.V1RepositoryDatabase.class); return V1RepositoryHelper.readV1Database(contextProvider, V1_REPOSITORY_FILENAME)
return readV1Database(jaxbContext)
.map(v1Database -> v1Database.repositoryList.repositories.stream()) .map(v1Database -> v1Database.repositoryList.repositories.stream())
.orElse(Stream.empty()) .orElse(Stream.empty())
.filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent()) .filter(v1Repository -> !this.findMigrationStrategy(v1Repository).isPresent())
@@ -196,33 +188,4 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep {
return new RepositoryPermission(v1Permission.getName(), v1Permission.getType(), v1Permission.isGroupPermission()); 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

@@ -198,9 +198,9 @@
} }
}, },
"namespaceStrategies": { "namespaceStrategies": {
"UsernameNamespaceStrategy": "Username", "UsernameNamespaceStrategy": "Username",
"CustomNamespaceStrategy": "Custom", "CustomNamespaceStrategy": "Custom",
"CurrentYearNamespaceStrategy": "Current year", "CurrentYearNamespaceStrategy": "Current year",
"RepositoryTypeNamespaceStrategy": "Repository type" "RepositoryTypeNamespaceStrategy": "Repository type"
} }
} }

View File

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

View File

@@ -70,7 +70,19 @@ class SetupContextListenerTest {
} }
@Test @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"); when(passwordService.encryptPassword("scmadmin")).thenReturn("secret");
setupContextListener.contextInitialized(null); setupContextListener.contextInitialized(null);

View File

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