From 0918d18d5eff072966f89038a84ed3309f155722 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 5 Jun 2019 18:36:14 +0200 Subject: [PATCH 001/106] fix autocompletion --- .../components/GroupAutocomplete.js | 56 +++++++++++++++++ .../components/UserAutocomplete.js | 56 +++++++++++++++++ .../containers/CreatePermissionForm.js | 63 ++++--------------- .../permissions/containers/Permissions.js | 20 +++--- 4 files changed, 135 insertions(+), 60 deletions(-) create mode 100644 scm-ui/src/repos/permissions/components/GroupAutocomplete.js create mode 100644 scm-ui/src/repos/permissions/components/UserAutocomplete.js diff --git a/scm-ui/src/repos/permissions/components/GroupAutocomplete.js b/scm-ui/src/repos/permissions/components/GroupAutocomplete.js new file mode 100644 index 0000000000..a8789b45ab --- /dev/null +++ b/scm-ui/src/repos/permissions/components/GroupAutocomplete.js @@ -0,0 +1,56 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Autocomplete } from "@scm-manager/ui-components"; +import type { SelectValue } from "@scm-manager/ui-types"; + +type Props = { + groupAutocompleteLink: string, + valueSelected: SelectValue => void, + value: string, + + // Context props + t: string => string +}; + +class GroupAutocomplete extends React.Component { + loadGroupSuggestions = (inputValue: string) => { + const url = this.props.groupAutocompleteLink; + const link = url + "?q="; + return fetch(link + inputValue) + .then(response => response.json()) + .then(json => { + return json.map(element => { + const label = element.displayName + ? `${element.displayName} (${element.id})` + : element.id; + return { + value: element, + label + }; + }); + }); + }; + + selectName = (selection: SelectValue) => { + this.props.valueSelected(selection); + }; + + render() { + const { t, value } = this.props; + return ( + + ); + } +} + +export default translate("repos")(GroupAutocomplete); diff --git a/scm-ui/src/repos/permissions/components/UserAutocomplete.js b/scm-ui/src/repos/permissions/components/UserAutocomplete.js new file mode 100644 index 0000000000..f3defadf0c --- /dev/null +++ b/scm-ui/src/repos/permissions/components/UserAutocomplete.js @@ -0,0 +1,56 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Autocomplete } from "@scm-manager/ui-components"; +import type { SelectValue } from "@scm-manager/ui-types"; + +type Props = { + userAutocompleteLink: string, + valueSelected: SelectValue => void, + value: string, + + // Context props + t: string => string +}; + +class UserAutocomplete extends React.Component { + loadUserSuggestions = (inputValue: string) => { + const url = this.props.userAutocompleteLink; + const link = url + "?q="; + return fetch(link + inputValue) + .then(response => response.json()) + .then(json => { + return json.map(element => { + const label = element.displayName + ? `${element.displayName} (${element.id})` + : element.id; + return { + value: element, + label + }; + }); + }); + }; + + selectName = (selection: SelectValue) => { + this.props.valueSelected(selection); + }; + + render() { + const { t, value } = this.props; + return ( + + ); + } +} + +export default translate("repos")(UserAutocomplete); diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 112ecd1c20..b3264a9ad0 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -9,7 +9,6 @@ import type { } from "@scm-manager/ui-types"; import { Subtitle, - Autocomplete, SubmitButton, Button, LabelWithHelpIcon, @@ -17,6 +16,8 @@ import { } from "@scm-manager/ui-components"; import * as validator from "../components/permissionValidation"; import RoleSelector from "../components/RoleSelector"; +import GroupAutocomplete from "../components/GroupAutocomplete"; +import UserAutocomplete from "../components/UserAutocomplete"; import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; import { findVerbsForRole } from "../modules/permissions"; @@ -26,8 +27,8 @@ type Props = { createPermission: (permission: PermissionCreateEntry) => void, loading: boolean, currentPermissions: PermissionCollection, - groupAutoCompleteLink: string, - userAutoCompleteLink: string, + groupAutocompleteLink: string, + userAutocompleteLink: string, // Context props t: string => string @@ -68,65 +69,27 @@ class CreatePermissionForm extends React.Component { }); }; - loadUserAutocompletion = (inputValue: string) => { - return this.loadAutocompletion(this.props.userAutoCompleteLink, inputValue); - }; - - loadGroupAutocompletion = (inputValue: string) => { - return this.loadAutocompletion( - this.props.groupAutoCompleteLink, - inputValue - ); - }; - - loadAutocompletion(url: string, inputValue: string) { - const link = url + "?q="; - return fetch(link + inputValue) - .then(response => response.json()) - .then(json => { - return json.map(element => { - const label = element.displayName - ? `${element.displayName} (${element.id})` - : element.id; - return { - value: element, - label - }; - }); - }); - } - renderAutocompletionField = () => { - const { t } = this.props; - if (this.state.groupPermission) { + const group = this.state.groupPermission; + if (group) { return ( - ); } return ( - ); }; - groupOrUserSelected = (value: SelectValue) => { +selectName = (value: SelectValue) => { this.setState({ value, name: value.value.id, diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 39bd9e37c1..ea467a56da 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -60,8 +60,8 @@ type Props = { repositoryRolesLink: string, repositoryVerbsLink: string, permissionsLink: string, - groupAutoCompleteLink: string, - userAutoCompleteLink: string, + groupAutocompleteLink: string, + userAutocompleteLink: string, //dispatch functions fetchAvailablePermissionsIfNeeded: ( @@ -129,8 +129,8 @@ class Permissions extends React.Component { repoName, loadingCreatePermission, hasPermissionToCreate, - userAutoCompleteLink, - groupAutoCompleteLink + userAutocompleteLink, + groupAutocompleteLink } = this.props; if (error) { return ( @@ -153,8 +153,8 @@ class Permissions extends React.Component { createPermission={permission => this.createPermission(permission)} loading={loadingCreatePermission} currentPermissions={permissions} - userAutoCompleteLink={userAutoCompleteLink} - groupAutoCompleteLink={groupAutoCompleteLink} + userAutocompleteLink={userAutocompleteLink} + groupAutocompleteLink={groupAutocompleteLink} /> ) : null; @@ -228,8 +228,8 @@ const mapStateToProps = (state, ownProps) => { const repositoryRolesLink = getRepositoryRolesLink(state); const repositoryVerbsLink = getRepositoryVerbsLink(state); const permissionsLink = getPermissionsLink(state, namespace, repoName); - const groupAutoCompleteLink = getGroupAutoCompleteLink(state); - const userAutoCompleteLink = getUserAutoCompleteLink(state); + const groupAutocompleteLink = getGroupAutoCompleteLink(state); + const userAutocompleteLink = getUserAutoCompleteLink(state); const availablePermissions = getAvailablePermissions(state); const availableRepositoryRoles = getAvailableRepositoryRoles(state); const availableVerbs = getAvailableRepositoryVerbs(state); @@ -248,8 +248,8 @@ const mapStateToProps = (state, ownProps) => { hasPermissionToCreate, loadingCreatePermission, permissionsLink, - groupAutoCompleteLink, - userAutoCompleteLink + groupAutocompleteLink, + userAutocompleteLink }; }; From 2d51506a657d3e97d2c1cba9c512a91828918370 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 11 Jun 2019 17:16:01 +0200 Subject: [PATCH 002/106] simplify toggleAdvancedPermission --- .../containers/CreatePermissionForm.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index b3264a9ad0..0113fae146 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -89,7 +89,7 @@ class CreatePermissionForm extends React.Component { ); }; -selectName = (value: SelectValue) => { + selectName = (value: SelectValue) => { this.setState({ value, name: value.value.id, @@ -113,15 +113,17 @@ selectName = (value: SelectValue) => { ) : null; return ( -
+ <>
- + {advancedDialog}
@@ -164,7 +166,7 @@ selectName = (value: SelectValue) => { />
@@ -180,16 +182,14 @@ selectName = (value: SelectValue) => { - + ); } - handleDetailedPermissionsPressed = () => { - this.setState({ showAdvancedDialog: true }); - }; - - closeAdvancedPermissionsDialog = () => { - this.setState({ showAdvancedDialog: false }); + toggleAdvancedPermissionsDialog = () => { + this.setState(prevState => ({ + showAdvancedDialog: !prevState.showAdvancedDialog + })); }; submitAdvancedPermissionsDialog = (newVerbs: string[]) => { From a1945b3f895360f938488f984cf107e0b79466af Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 11 Jun 2019 18:12:49 +0200 Subject: [PATCH 003/106] add missing trans --- scm-ui/public/locales/de/repos.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index cf4f066120..4f85ce0fde 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -11,7 +11,10 @@ "validation": { "namespace-invalid": "Der Namespace des Repository ist ungültig", "name-invalid": "Der Name des Repository ist ungültig", - "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein" + "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein", + "branch": { + "nameInvalid": "Der Name des Branches ist ungültig" + } }, "help": { "namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.", From 5fd9408b585037c44c9392b530bbafcfe872cc4b Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 09:27:26 +0200 Subject: [PATCH 004/106] fix element types --- .../ui-components/src/Autocomplete.js | 28 ++++++++++++------- .../ui-components/src/GroupAutocomplete.js | 2 +- .../ui-components/src/UserAutocomplete.js | 2 +- .../src/UserGroupAutocomplete.js | 18 ++++++++---- scm-ui/public/locales/de/commons.json | 8 +++--- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/Autocomplete.js b/scm-ui-components/packages/ui-components/src/Autocomplete.js index a96eaaaa3c..22107e75b6 100644 --- a/scm-ui-components/packages/ui-components/src/Autocomplete.js +++ b/scm-ui-components/packages/ui-components/src/Autocomplete.js @@ -16,12 +16,9 @@ type Props = { creatable?: boolean }; - type State = {}; class Autocomplete extends React.Component { - - static defaultProps = { placeholder: "Type here", loadingMessage: "Loading...", @@ -33,7 +30,11 @@ class Autocomplete extends React.Component { }; // We overwrite this to avoid running into a bug (https://github.com/JedWatson/react-select/issues/2944) - isValidNewOption = (inputValue: string, selectValue: SelectValue, selectOptions: SelectValue[]) => { + isValidNewOption = ( + inputValue: string, + selectValue: SelectValue, + selectOptions: SelectValue[] + ) => { const isNotDuplicated = !selectOptions .map(option => option.label) .includes(inputValue); @@ -42,12 +43,21 @@ class Autocomplete extends React.Component { }; render() { - const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions, creatable } = this.props; + const { + label, + helpText, + value, + placeholder, + loadingMessage, + noOptionsMessage, + loadSuggestions, + creatable + } = this.props; return (
- {creatable? + {creatable ? ( { }); }} /> - : + ) : ( { loadingMessage={() => loadingMessage} noOptionsMessage={() => noOptionsMessage} /> - - } + )}
); } } - export default Autocomplete; diff --git a/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js index 319543e22d..2a9feeec14 100644 --- a/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js @@ -7,7 +7,7 @@ import UserGroupAutocomplete from "./UserGroupAutocomplete"; type Props = { groupAutocompleteLink: string, valueSelected: SelectValue => void, - value: string, + value?: SelectValue, // Context props t: string => string diff --git a/scm-ui-components/packages/ui-components/src/UserAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js index fe7e449bfb..3f9873608c 100644 --- a/scm-ui-components/packages/ui-components/src/UserAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js @@ -7,7 +7,7 @@ import UserGroupAutocomplete from "./UserGroupAutocomplete"; type Props = { userAutocompleteLink: string, valueSelected: SelectValue => void, - value: string, + value?: SelectValue, // Context props t: string => string diff --git a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js index fefb6dd643..a6469c831e 100644 --- a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js @@ -5,9 +5,12 @@ import Autocomplete from "./Autocomplete"; type Props = { autocompleteLink: string, + label: string, + noOptionsMessage: string, + loadingMessage: string, + placeholder: string, valueSelected: SelectValue => void, - value: string, - label: string + value?: SelectValue }; class UserGroupAutocomplete extends React.Component { @@ -34,17 +37,20 @@ class UserGroupAutocomplete extends React.Component { }; render() { - const { value, label } = this.props; + const { autocompleteLink, label, noOptionsMessage, loadingMessage, placeholder, value } = this.props; return ( - ); + ); // {...this.props} } } diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index 0ce6ebb81f..012c990833 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -43,10 +43,10 @@ "autocomplete": { "group": "Gruppe", "user": "Benutzer", - "no-group-options": "Kein Gruppenname als Vorschlag verfügbar", - "group-placeholder": "Gruppe eingeben", - "no-user-options": "Kein Benutzername als Vorschlag verfügbar", - "user-placeholder": "Benutzer eingeben", + "noGroupOptions": "Kein Gruppenname als Vorschlag verfügbar", + "groupPlaceholder": "Gruppe eingeben", + "noUserOptions": "Kein Benutzername als Vorschlag verfügbar", + "userPlaceholder": "Benutzer eingeben", "loading": "suche..." }, "paginator": { From 303e7f1e50239fd0968a329493a5163645c46308 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 09:47:13 +0200 Subject: [PATCH 005/106] fix autocompletion --- .../ui-components/src/Autocomplete.js | 29 ++++--- .../ui-components/src/GroupAutocomplete.js | 37 ++++++++ .../ui-components/src/UserAutocomplete.js | 37 ++++++++ .../src/UserGroupAutocomplete.js | 57 ++++++++++++ scm-ui/public/locales/de/commons.json | 9 ++ scm-ui/public/locales/de/repos.json | 7 -- scm-ui/public/locales/en/commons.json | 9 ++ scm-ui/public/locales/en/repos.json | 7 -- .../containers/CreatePermissionForm.js | 87 ++++++------------- .../permissions/containers/Permissions.js | 86 +++++++++--------- 10 files changed, 235 insertions(+), 130 deletions(-) create mode 100644 scm-ui-components/packages/ui-components/src/GroupAutocomplete.js create mode 100644 scm-ui-components/packages/ui-components/src/UserAutocomplete.js create mode 100644 scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js diff --git a/scm-ui-components/packages/ui-components/src/Autocomplete.js b/scm-ui-components/packages/ui-components/src/Autocomplete.js index adf86e37b7..22107e75b6 100644 --- a/scm-ui-components/packages/ui-components/src/Autocomplete.js +++ b/scm-ui-components/packages/ui-components/src/Autocomplete.js @@ -4,7 +4,6 @@ import { AsyncCreatable, Async } from "react-select"; import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; - type Props = { loadSuggestions: string => Promise, valueSelected: SelectValue => void, @@ -17,12 +16,9 @@ type Props = { creatable?: boolean }; - type State = {}; class Autocomplete extends React.Component { - - static defaultProps = { placeholder: "Type here", loadingMessage: "Loading...", @@ -34,7 +30,11 @@ class Autocomplete extends React.Component { }; // We overwrite this to avoid running into a bug (https://github.com/JedWatson/react-select/issues/2944) - isValidNewOption = (inputValue: string, selectValue: SelectValue, selectOptions: SelectValue[]) => { + isValidNewOption = ( + inputValue: string, + selectValue: SelectValue, + selectOptions: SelectValue[] + ) => { const isNotDuplicated = !selectOptions .map(option => option.label) .includes(inputValue); @@ -43,12 +43,21 @@ class Autocomplete extends React.Component { }; render() { - const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions, creatable } = this.props; + const { + label, + helpText, + value, + placeholder, + loadingMessage, + noOptionsMessage, + loadSuggestions, + creatable + } = this.props; return (
- {creatable? + {creatable ? ( { }); }} /> - : + ) : ( { loadingMessage={() => loadingMessage} noOptionsMessage={() => noOptionsMessage} /> - - } + )}
); } } - export default Autocomplete; diff --git a/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js new file mode 100644 index 0000000000..2a9feeec14 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js @@ -0,0 +1,37 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { SelectValue } from "@scm-manager/ui-types"; +import UserGroupAutocomplete from "./UserGroupAutocomplete"; + +type Props = { + groupAutocompleteLink: string, + valueSelected: SelectValue => void, + value?: SelectValue, + + // Context props + t: string => string +}; + +class GroupAutocomplete extends React.Component { + selectName = (selection: SelectValue) => { + this.props.valueSelected(selection); + }; + + render() { + const { groupAutocompleteLink, t, value } = this.props; + return ( + + ); + } +} + +export default translate("commons")(GroupAutocomplete); diff --git a/scm-ui-components/packages/ui-components/src/UserAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js new file mode 100644 index 0000000000..3f9873608c --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js @@ -0,0 +1,37 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { SelectValue } from "@scm-manager/ui-types"; +import UserGroupAutocomplete from "./UserGroupAutocomplete"; + +type Props = { + userAutocompleteLink: string, + valueSelected: SelectValue => void, + value?: SelectValue, + + // Context props + t: string => string +}; + +class UserAutocomplete extends React.Component { + selectName = (selection: SelectValue) => { + this.props.valueSelected(selection); + }; + + render() { + const { userAutocompleteLink, t, value } = this.props; + return ( + + ); + } +} + +export default translate("commons")(UserAutocomplete); diff --git a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js new file mode 100644 index 0000000000..a6469c831e --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js @@ -0,0 +1,57 @@ +// @flow +import React from "react"; +import type { SelectValue } from "@scm-manager/ui-types"; +import Autocomplete from "./Autocomplete"; + +type Props = { + autocompleteLink: string, + label: string, + noOptionsMessage: string, + loadingMessage: string, + placeholder: string, + valueSelected: SelectValue => void, + value?: SelectValue +}; + +class UserGroupAutocomplete extends React.Component { + loadSuggestions = (inputValue: string) => { + const url = this.props.autocompleteLink; + const link = url + "?q="; + return fetch(link + inputValue) + .then(response => response.json()) + .then(json => { + return json.map(element => { + const label = element.displayName + ? `${element.displayName} (${element.id})` + : element.id; + return { + value: element, + label + }; + }); + }); + }; + + selectName = (selection: SelectValue) => { + this.props.valueSelected(selection); + }; + + render() { + const { autocompleteLink, label, noOptionsMessage, loadingMessage, placeholder, value } = this.props; + return ( + + ); // {...this.props} + } +} + +export default UserGroupAutocomplete; diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index b9cc93b1ed..7c0e326507 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -40,6 +40,15 @@ "config": "Einstellungen" }, "filterEntries": "Einträge filtern", + "autocomplete": { + "group": "Gruppe", + "user": "Benutzer", + "noGroupOptions": "Kein Gruppenname als Vorschlag verfügbar", + "groupPlaceholder": "Gruppe eingeben", + "noUserOptions": "Kein Benutzername als Vorschlag verfügbar", + "userPlaceholder": "Benutzer eingeben", + "loading": "suche..." + }, "paginator": { "next": "Weiter", "previous": "Zurück" diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index 4ba7a725e6..cf4f066120 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -150,13 +150,6 @@ "roleHelpText": "READ = read; WRITE = read und write; OWNER = read, write und auch die Möglichkeit Einstellungen und Berechtigungen zu verwalten. Wenn hier nichts angezeigt wird, den Erweitert-Button benutzen, um Details zu sehen.", "permissionsHelpText": "Hier können individuelle Berechtigungen unabhängig von vordefinierten Rollen vergeben werden." }, - "autocomplete": { - "no-group-options": "Kein Gruppenname als Vorschlag verfügbar", - "group-placeholder": "Gruppe eingeben", - "no-user-options": "Kein Benutzername als Vorschlag verfügbar", - "user-placeholder": "Benutzer eingeben", - "loading": "suche..." - }, "advanced": { "dialog": { "title": "Erweiterte Berechtigungen", diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index b5af3e9ef9..d360b557f2 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -40,6 +40,15 @@ "config": "Configuration" }, "filterEntries": "filter entries", + "autocomplete": { + "group": "Group", + "user": "User", + "noGroupOptions": "No group suggestion available", + "groupPlaceholder": "Enter group", + "noUserOptions": "No user suggestion available", + "userPlaceholder": "Enter user", + "loading": "Loading..." + }, "paginator": { "next": "Next", "previous": "Previous" diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 9a2e83f983..a312287a68 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -153,13 +153,6 @@ "roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.", "permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles." }, - "autocomplete": { - "no-group-options": "No group suggestion available", - "group-placeholder": "Enter group", - "no-user-options": "No user suggestion available", - "user-placeholder": "Enter user", - "loading": "Loading..." - }, "advanced": { "dialog": { "title": "Advanced Permissions", diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 112ecd1c20..51b43fc2ff 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -9,11 +9,12 @@ import type { } from "@scm-manager/ui-types"; import { Subtitle, - Autocomplete, SubmitButton, Button, LabelWithHelpIcon, - Radio + Radio, + GroupAutocomplete, + UserAutocomplete } from "@scm-manager/ui-components"; import * as validator from "../components/permissionValidation"; import RoleSelector from "../components/RoleSelector"; @@ -26,8 +27,8 @@ type Props = { createPermission: (permission: PermissionCreateEntry) => void, loading: boolean, currentPermissions: PermissionCollection, - groupAutoCompleteLink: string, - userAutoCompleteLink: string, + groupAutocompleteLink: string, + userAutocompleteLink: string, // Context props t: string => string @@ -68,65 +69,27 @@ class CreatePermissionForm extends React.Component { }); }; - loadUserAutocompletion = (inputValue: string) => { - return this.loadAutocompletion(this.props.userAutoCompleteLink, inputValue); - }; - - loadGroupAutocompletion = (inputValue: string) => { - return this.loadAutocompletion( - this.props.groupAutoCompleteLink, - inputValue - ); - }; - - loadAutocompletion(url: string, inputValue: string) { - const link = url + "?q="; - return fetch(link + inputValue) - .then(response => response.json()) - .then(json => { - return json.map(element => { - const label = element.displayName - ? `${element.displayName} (${element.id})` - : element.id; - return { - value: element, - label - }; - }); - }); - } - renderAutocompletionField = () => { - const { t } = this.props; - if (this.state.groupPermission) { + const group = this.state.groupPermission; + if (group) { return ( - ); } return ( - ); }; - groupOrUserSelected = (value: SelectValue) => { + selectName = (value: SelectValue) => { this.setState({ value, name: value.value.id, @@ -150,15 +113,17 @@ class CreatePermissionForm extends React.Component { ) : null; return ( -
+ <>
- + {advancedDialog}
@@ -201,7 +166,7 @@ class CreatePermissionForm extends React.Component { />
@@ -217,16 +182,14 @@ class CreatePermissionForm extends React.Component { - + ); } - handleDetailedPermissionsPressed = () => { - this.setState({ showAdvancedDialog: true }); - }; - - closeAdvancedPermissionsDialog = () => { - this.setState({ showAdvancedDialog: false }); + toggleAdvancedPermissionsDialog = () => { + this.setState(prevState => ({ + showAdvancedDialog: !prevState.showAdvancedDialog + })); }; submitAdvancedPermissionsDialog = (newVerbs: string[]) => { diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 39bd9e37c1..7d44328305 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -60,8 +60,8 @@ type Props = { repositoryRolesLink: string, repositoryVerbsLink: string, permissionsLink: string, - groupAutoCompleteLink: string, - userAutoCompleteLink: string, + groupAutocompleteLink: string, + userAutocompleteLink: string, //dispatch functions fetchAvailablePermissionsIfNeeded: ( @@ -129,8 +129,8 @@ class Permissions extends React.Component { repoName, loadingCreatePermission, hasPermissionToCreate, - userAutoCompleteLink, - groupAutoCompleteLink + userAutocompleteLink, + groupAutocompleteLink } = this.props; if (error) { return ( @@ -153,8 +153,8 @@ class Permissions extends React.Component { createPermission={permission => this.createPermission(permission)} loading={loadingCreatePermission} currentPermissions={permissions} - userAutoCompleteLink={userAutoCompleteLink} - groupAutoCompleteLink={groupAutoCompleteLink} + userAutocompleteLink={userAutocompleteLink} + groupAutocompleteLink={groupAutocompleteLink} /> ) : null; @@ -163,41 +163,41 @@ class Permissions extends React.Component { - - - - - + + + + + - {permissions.map(permission => { - return ( - - ); - })} + {permissions.map(permission => { + return ( + + ); + })}
- - - - - - -
+ + + + + + +
{createPermissionForm} @@ -228,8 +228,8 @@ const mapStateToProps = (state, ownProps) => { const repositoryRolesLink = getRepositoryRolesLink(state); const repositoryVerbsLink = getRepositoryVerbsLink(state); const permissionsLink = getPermissionsLink(state, namespace, repoName); - const groupAutoCompleteLink = getGroupAutoCompleteLink(state); - const userAutoCompleteLink = getUserAutoCompleteLink(state); + const groupAutocompleteLink = getGroupAutoCompleteLink(state); + const userAutocompleteLink = getUserAutoCompleteLink(state); const availablePermissions = getAvailablePermissions(state); const availableRepositoryRoles = getAvailableRepositoryRoles(state); const availableVerbs = getAvailableRepositoryVerbs(state); @@ -248,8 +248,8 @@ const mapStateToProps = (state, ownProps) => { hasPermissionToCreate, loadingCreatePermission, permissionsLink, - groupAutoCompleteLink, - userAutoCompleteLink + groupAutocompleteLink, + userAutocompleteLink }; }; From dbcf130f4bde9daa034121607fd6bdf7a0522e89 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 09:51:55 +0200 Subject: [PATCH 006/106] simplify passed parms --- .../ui-components/src/UserGroupAutocomplete.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js index a6469c831e..e0af92f205 100644 --- a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js @@ -5,12 +5,7 @@ import Autocomplete from "./Autocomplete"; type Props = { autocompleteLink: string, - label: string, - noOptionsMessage: string, - loadingMessage: string, - placeholder: string, - valueSelected: SelectValue => void, - value?: SelectValue + valueSelected: SelectValue => void }; class UserGroupAutocomplete extends React.Component { @@ -37,20 +32,14 @@ class UserGroupAutocomplete extends React.Component { }; render() { - const { autocompleteLink, label, noOptionsMessage, loadingMessage, placeholder, value } = this.props; return ( - ); // {...this.props} + ); } } From c1218e221889eff5e382743778cb6d2b34fd9ec1 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 09:52:08 +0200 Subject: [PATCH 007/106] fix export --- scm-ui-components/packages/ui-components/src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 954b0b0955..b4f78c8cf0 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -25,6 +25,8 @@ export { default as Tooltip } from "./Tooltip"; // TODO do we need this? getPageFromMatch is already exported by urls export { getPageFromMatch } from "./urls"; export { default as Autocomplete} from "./Autocomplete"; +export { default as GroupAutocomplete} from "./GroupAutocomplete"; +export { default as UserAutocomplete} from "./UserAutocomplete"; export { default as BranchSelector } from "./BranchSelector"; export { default as MarkdownView } from "./MarkdownView"; export { default as SyntaxHighlighter } from "./SyntaxHighlighter"; From 037310c73fc5ba99aae16df8067d2297a0e2dfc5 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 09:55:52 +0200 Subject: [PATCH 008/106] add missing trans --- scm-ui/public/locales/de/repos.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index cf4f066120..4f85ce0fde 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -11,7 +11,10 @@ "validation": { "namespace-invalid": "Der Namespace des Repository ist ungültig", "name-invalid": "Der Name des Repository ist ungültig", - "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein" + "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein", + "branch": { + "nameInvalid": "Der Name des Branches ist ungültig" + } }, "help": { "namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.", From 11b5f81e24fafa2384b356a9bd40f2d36f88585b Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 09:56:47 +0200 Subject: [PATCH 009/106] prettier --- .../permissions/containers/Permissions.js | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 7d44328305..ea467a56da 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -163,41 +163,41 @@ class Permissions extends React.Component { - - - - - + + + + + - {permissions.map(permission => { - return ( - - ); - })} + {permissions.map(permission => { + return ( + + ); + })}
- - - - - - -
+ + + + + + +
{createPermissionForm} From 252034f30b818205bb7bb28e45d74affbfa0defb Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 12 Jun 2019 07:59:21 +0000 Subject: [PATCH 010/106] Close branch bugfix/autocomplete. From ff354a5feeb2631b73115a7169bf51b1212638e7 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 18 Jun 2019 15:20:44 +0200 Subject: [PATCH 011/106] review changes --- .../ui-components/src/GroupAutocomplete.js | 18 +++---------- .../ui-components/src/UserAutocomplete.js | 18 +++---------- .../src/UserGroupAutocomplete.js | 27 ++++++++----------- .../containers/CreatePermissionForm.js | 4 +-- 4 files changed, 21 insertions(+), 46 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js index 2a9feeec14..e39130f1d7 100644 --- a/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/GroupAutocomplete.js @@ -1,34 +1,24 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import type { SelectValue } from "@scm-manager/ui-types"; +import type AutocompleteProps from "./UserGroupAutocomplete"; import UserGroupAutocomplete from "./UserGroupAutocomplete"; -type Props = { - groupAutocompleteLink: string, - valueSelected: SelectValue => void, - value?: SelectValue, - +type Props = AutocompleteProps & { // Context props t: string => string }; class GroupAutocomplete extends React.Component { - selectName = (selection: SelectValue) => { - this.props.valueSelected(selection); - }; - render() { - const { groupAutocompleteLink, t, value } = this.props; + const { t } = this.props; return ( ); } diff --git a/scm-ui-components/packages/ui-components/src/UserAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js index 3f9873608c..9ef7aaa7a7 100644 --- a/scm-ui-components/packages/ui-components/src/UserAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/UserAutocomplete.js @@ -1,34 +1,24 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import type { SelectValue } from "@scm-manager/ui-types"; +import type AutocompleteProps from "./UserGroupAutocomplete"; import UserGroupAutocomplete from "./UserGroupAutocomplete"; -type Props = { - userAutocompleteLink: string, - valueSelected: SelectValue => void, - value?: SelectValue, - +type Props = AutocompleteProps & { // Context props t: string => string }; class UserAutocomplete extends React.Component { - selectName = (selection: SelectValue) => { - this.props.valueSelected(selection); - }; - render() { - const { userAutocompleteLink, t, value } = this.props; + const { t } = this.props; return ( ); } diff --git a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js index a6469c831e..0d6e3ec46e 100644 --- a/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js +++ b/scm-ui-components/packages/ui-components/src/UserGroupAutocomplete.js @@ -3,17 +3,20 @@ import React from "react"; import type { SelectValue } from "@scm-manager/ui-types"; import Autocomplete from "./Autocomplete"; -type Props = { +export type AutocompleteProps = { autocompleteLink: string, - label: string, - noOptionsMessage: string, - loadingMessage: string, - placeholder: string, valueSelected: SelectValue => void, value?: SelectValue }; -class UserGroupAutocomplete extends React.Component { +type Props = AutocompleteProps & { + label: string, + noOptionsMessage: string, + loadingMessage: string, + placeholder: string +}; + +export default class UserGroupAutocomplete extends React.Component { loadSuggestions = (inputValue: string) => { const url = this.props.autocompleteLink; const link = url + "?q="; @@ -37,21 +40,13 @@ class UserGroupAutocomplete extends React.Component { }; render() { - const { autocompleteLink, label, noOptionsMessage, loadingMessage, placeholder, value } = this.props; return ( - ); // {...this.props} + ); } } - -export default UserGroupAutocomplete; diff --git a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 51b43fc2ff..efd92914d3 100644 --- a/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -74,7 +74,7 @@ class CreatePermissionForm extends React.Component { if (group) { return ( @@ -82,7 +82,7 @@ class CreatePermissionForm extends React.Component { } return ( From 7db3350439382d1cf6f00cba37edd93c8e687a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 1 Jul 2019 14:17:32 +0200 Subject: [PATCH 012/106] Migrate git LFS blob directory --- .../sonia/scm/update/BlobDirectoryAccess.java | 15 ++++ .../scm/store/DefaultBlobDirectoryAccess.java | 68 +++++++++++++++++++ .../scm/store/JAXBPropertyFileAccess.java | 9 ++- .../sonia/scm/web/lfs/LfsV1UpdateStep.java | 43 ++++++++++++ .../lifecycle/modules/BootstrapModule.java | 3 + 5 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java create mode 100644 scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java diff --git a/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java b/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java new file mode 100644 index 0000000000..35eaa134fd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/BlobDirectoryAccess.java @@ -0,0 +1,15 @@ +package sonia.scm.update; + +import java.io.IOException; +import java.nio.file.Path; + +public interface BlobDirectoryAccess { + + void forBlobDirectories(BlobDirectoryConsumer blobDirectoryConsumer) throws IOException; + + void moveToRepositoryBlobStore(Path blobDirectory, String newDirectoryName, String repositoryId) throws IOException; + + interface BlobDirectoryConsumer { + void accept(Path directory) throws IOException; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java b/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java new file mode 100644 index 0000000000..d22a76c79d --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/DefaultBlobDirectoryAccess.java @@ -0,0 +1,68 @@ +package sonia.scm.store; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.BlobDirectoryAccess; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class DefaultBlobDirectoryAccess implements BlobDirectoryAccess { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultBlobDirectoryAccess.class); + + private final SCMContextProvider contextProvider; + private final RepositoryLocationResolver locationResolver; + + @Inject + public DefaultBlobDirectoryAccess(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { + this.contextProvider = contextProvider; + this.locationResolver = locationResolver; + } + + @Override + public void forBlobDirectories(BlobDirectoryConsumer blobDirectoryConsumer) throws IOException { + Path v1blobDir = computeV1BlobDir(); + if (Files.exists(v1blobDir) && Files.isDirectory(v1blobDir)) { + try (Stream fileStream = Files.list(v1blobDir)) { + fileStream.filter(p -> Files.isDirectory(p)).forEach(p -> { + try { + blobDirectoryConsumer.accept(p); + } catch (IOException e) { + throw new RuntimeException("could not call consumer for blob directory " + p, e); + } + }); + } + } + } + + @Override + public void moveToRepositoryBlobStore(Path blobDirectory, String newDirectoryName, String repositoryId) throws IOException { + Path repositoryLocation; + try { + repositoryLocation = locationResolver + .forClass(Path.class) + .getLocation(repositoryId); + } catch (IllegalStateException e) { + LOG.info("ignoring blob directory {} because there is no repository location for repository id {}", blobDirectory, repositoryId); + return; + } + Path target = repositoryLocation + .resolve(Store.BLOB.getRepositoryStoreDirectory()); + IOUtil.mkdirs(target.toFile()); + Path resolvedSourceDirectory = computeV1BlobDir().resolve(blobDirectory); + Path resolvedTargetDirectory = target.resolve(newDirectoryName); + LOG.trace("moving directory {} to {}", resolvedSourceDirectory, resolvedTargetDirectory); + Files.move(resolvedSourceDirectory, resolvedTargetDirectory); + } + + private Path computeV1BlobDir() { + return contextProvider.getBaseDirectory().toPath().resolve("var").resolve("blob"); + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java index b8d30bc0c1..ca4aeea468 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java @@ -17,7 +17,6 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess { private static final Logger LOG = LoggerFactory.getLogger(JAXBPropertyFileAccess.class); - public static final String XML_FILENAME_SUFFIX = ".xml"; private final SCMContextProvider contextProvider; private final RepositoryLocationResolver locationResolver; @@ -31,8 +30,8 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess { public Target renameGlobalConfigurationFrom(String oldName) { return newName -> { Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME); - Path oldConfigFile = configDir.resolve(oldName + XML_FILENAME_SUFFIX); - Path newConfigFile = configDir.resolve(newName + XML_FILENAME_SUFFIX); + Path oldConfigFile = configDir.resolve(oldName + StoreConstants.FILE_EXTENSION); + Path newConfigFile = configDir.resolve(newName + StoreConstants.FILE_EXTENSION); Files.move(oldConfigFile, newConfigFile); }; } @@ -45,7 +44,7 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess { Path v1storeDir = computeV1StoreDir(); if (Files.exists(v1storeDir) && Files.isDirectory(v1storeDir)) { try (Stream fileStream = Files.list(v1storeDir)) { - fileStream.filter(p -> p.toString().endsWith(XML_FILENAME_SUFFIX)).forEach(p -> { + fileStream.filter(p -> p.toString().endsWith(StoreConstants.FILE_EXTENSION)).forEach(p -> { try { String storeName = extractStoreName(p); storeFileConsumer.accept(p, storeName); @@ -84,7 +83,7 @@ public class JAXBPropertyFileAccess implements PropertyFileAccess { private String extractStoreName(Path p) { String fileName = p.getFileName().toString(); - return fileName.substring(0, fileName.length() - XML_FILENAME_SUFFIX.length()); + return fileName.substring(0, fileName.length() - StoreConstants.FILE_EXTENSION.length()); } }; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java new file mode 100644 index 0000000000..2684222c32 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsV1UpdateStep.java @@ -0,0 +1,43 @@ +package sonia.scm.web.lfs; + +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.update.BlobDirectoryAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.nio.file.Path; + +@Extension +public class LfsV1UpdateStep implements UpdateStep { + + private final BlobDirectoryAccess blobDirectoryAccess; + + @Inject + public LfsV1UpdateStep(BlobDirectoryAccess blobDirectoryAccess) { + this.blobDirectoryAccess = blobDirectoryAccess; + } + + @Override + public void doUpdate() throws Exception { + blobDirectoryAccess.forBlobDirectories( + f -> { + Path v1Directory = f.getFileName(); + String v1DirectoryName = v1Directory.toString(); + if (v1DirectoryName.endsWith("-git-lfs")) { + blobDirectoryAccess.moveToRepositoryBlobStore(f, v1DirectoryName, v1DirectoryName.substring(0, v1DirectoryName.length() - "-git-lfs".length())); + } + } + ); + } + + @Override + public Version getTargetVersion() { + return Version.parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.git.lfs"; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java index 9b6ea719d8..c0c933efd2 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java @@ -19,11 +19,13 @@ import sonia.scm.store.BlobStoreFactory; import sonia.scm.store.ConfigurationEntryStoreFactory; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.DataStoreFactory; +import sonia.scm.store.DefaultBlobDirectoryAccess; import sonia.scm.store.FileBlobStoreFactory; import sonia.scm.store.JAXBConfigurationEntryStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.store.JAXBDataStoreFactory; import sonia.scm.store.JAXBPropertyFileAccess; +import sonia.scm.update.BlobDirectoryAccess; import sonia.scm.update.PropertyFileAccess; import sonia.scm.update.V1PropertyDAO; import sonia.scm.update.xml.XmlV1PropertyDAO; @@ -65,6 +67,7 @@ public class BootstrapModule extends AbstractModule { bind(PluginLoader.class).toInstance(pluginLoader); bind(V1PropertyDAO.class, XmlV1PropertyDAO.class); bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class); + bind(BlobDirectoryAccess.class, DefaultBlobDirectoryAccess.class); } private void bind(Class clazz, Class defaultImplementation) { From f631dc17242c500c77489574eb89079a92468536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 2 Jul 2019 10:46:26 +0200 Subject: [PATCH 013/106] Redirect protocol requests for migrated repositories - Make old repository name and id available to plugins (MigrationDAO) - Build Map with paths of migrated repositories to new location - Implement redirect (RepositoryLegacyProtocolRedirectFilter) --- .../sonia/scm/migration/MigrationDAO.java | 7 + .../sonia/scm/migration/MigrationInfo.java | 38 ++++++ .../java/sonia/scm/legacy/LegacyModule.java | 13 ++ ...epositoryLegacyProtocolRedirectFilter.java | 126 ++++++++++++++++++ ...itoryLegacyProtocolRedirectFilterTest.java | 83 ++++++++++++ .../lifecycle/modules/ScmServletModule.java | 3 + .../scm/update/MigrationWizardServlet.java | 9 +- .../DefaultMigrationStrategyDAO.java | 44 ++++++ .../repository/MigrationStrategyDao.java | 30 ----- .../repository/RepositoryMigrationPlan.java | 22 ++- .../repository/XmlRepositoryV1UpdateStep.java | 6 +- .../update/MigrationWizardServletTest.java | 6 +- ...a => DefaultMigrationStrategyDAOTest.java} | 15 +-- .../XmlRepositoryV1UpdateStepTest.java | 6 +- 14 files changed, 353 insertions(+), 55 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java create mode 100644 scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java create mode 100644 scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java create mode 100644 scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java create mode 100644 scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java create mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/DefaultMigrationStrategyDAO.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java rename scm-webapp/src/test/java/sonia/scm/update/repository/{MigrationStrategyDaoTest.java => DefaultMigrationStrategyDAOTest.java} (83%) diff --git a/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java b/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java new file mode 100644 index 0000000000..acc37ce8ee --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/MigrationDAO.java @@ -0,0 +1,7 @@ +package sonia.scm.migration; + +import java.util.Collection; + +public interface MigrationDAO { + Collection getAll(); +} diff --git a/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java b/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java new file mode 100644 index 0000000000..19f5d8ba3a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/migration/MigrationInfo.java @@ -0,0 +1,38 @@ +package sonia.scm.migration; + +public class MigrationInfo { + + private final String id; + private final String protocol; + private final String originalRepositoryName; + private final String namespace; + private final String name; + + public MigrationInfo(String id, String protocol, String originalRepositoryName, String namespace, String name) { + this.id = id; + this.protocol = protocol; + this.originalRepositoryName = originalRepositoryName; + this.namespace = namespace; + this.name = name; + } + + public String getId() { + return id; + } + + public String getProtocol() { + return protocol; + } + + public String getOriginalRepositoryName() { + return originalRepositoryName; + } + + public String getNamespace() { + return namespace; + } + + public String getName() { + return name; + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java new file mode 100644 index 0000000000..e8ec437248 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyModule.java @@ -0,0 +1,13 @@ +package sonia.scm.legacy; + +import com.google.inject.servlet.ServletModule; +import sonia.scm.plugin.Extension; + +@Extension +public class LegacyModule extends ServletModule { + + @Override + protected void configureServlets() { + filter("/*").through(RepositoryLegacyProtocolRedirectFilter.class); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java new file mode 100644 index 0000000000..c008caf111 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java @@ -0,0 +1,126 @@ +package sonia.scm.legacy; + +import sonia.scm.migration.MigrationDAO; +import sonia.scm.migration.MigrationInfo; +import sonia.scm.web.filter.HttpFilter; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.Arrays.asList; +import static java.util.Optional.empty; +import static java.util.Optional.of; + +@Singleton +public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { + + private final ProtocolBasedLegacyRepositoryInfo info; + + @Inject + public RepositoryLegacyProtocolRedirectFilter(MigrationDAO migrationDAO) { + this.info = load(migrationDAO); + } + + private static ProtocolBasedLegacyRepositoryInfo load(MigrationDAO migrationDAO) { + return new ProtocolBasedLegacyRepositoryInfo(migrationDAO.getAll()); + } + + @Override + protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + String servletPath = request.getServletPath(); + if (servletPath.startsWith("/")) { + servletPath = servletPath.substring(1); + } + String[] pathElements = servletPath.split("/"); + if (pathElements.length > 0) { + Optional repository = info.findRepository(asList(pathElements)); + if (repository.isPresent()) { + String furtherPath = servletPath.substring(repository.get().getProtocol().length() + 1 + repository.get().getOriginalRepositoryName().length()); + String queryString = request.getQueryString(); + if (queryString != null && queryString.length() > 0) { + response.sendRedirect(String.format("%s/repo/%s/%s%s?%s", request.getContextPath(), repository.get().getNamespace(), repository.get().getName(), furtherPath, queryString)); + } else { + response.sendRedirect(String.format("%s/repo/%s/%s%s", request.getContextPath(), repository.get().getNamespace(), repository.get().getName(), furtherPath)); + } + } else { + chain.doFilter(request, response); + } + } else { + chain.doFilter(request, response); + } + } + + private static class ProtocolBasedLegacyRepositoryInfo { + + private final Map infosForProtocol = new HashMap<>(); + + public ProtocolBasedLegacyRepositoryInfo(Collection all) { + all.forEach(this::add); + } + + private void add(MigrationInfo migrationInfo) { + String protocol = migrationInfo.getProtocol(); + LegacyRepositoryInfoCollection legacyRepositoryInfoCollection = infosForProtocol.computeIfAbsent(protocol, x -> new LegacyRepositoryInfoCollection()); + legacyRepositoryInfoCollection.add(migrationInfo); + } + + private Optional findRepository(List pathElements) { + String protocol = pathElements.get(0); + if (!isProtocol(protocol)) { + return empty(); + } + return infosForProtocol.get(protocol).findRepository(removeFirstElement(pathElements)); + } + + public boolean isProtocol(String protocol) { + return infosForProtocol.containsKey(protocol); + } + } + + private static class LegacyRepositoryInfoCollection { + + private final Map repositories = new HashMap<>(); + private final Map next = new HashMap<>(); + + public Optional findRepository(List pathElements) { + if (repositories.containsKey(pathElements.get(0))) { + return of(repositories.get(pathElements.get(0))); + } else if (next.containsKey(pathElements.get(0))) { + return next.get(pathElements.get(0)).findRepository(removeFirstElement(pathElements)); + } else { + return empty(); + } + } + + private void add(MigrationInfo migrationInfo) { + String originalRepositoryName = migrationInfo.getOriginalRepositoryName(); + List originalRepositoryNameParts = asList(originalRepositoryName.split("/")); + add(migrationInfo, originalRepositoryNameParts); + } + + private void add(MigrationInfo migrationInfo, List originalRepositoryNameParts) { + if (originalRepositoryNameParts.isEmpty()) { + throw new IllegalArgumentException("cannot handle empty name"); + } else if (originalRepositoryNameParts.size() == 1) { + repositories.put(originalRepositoryNameParts.get(0), migrationInfo); + } else { + LegacyRepositoryInfoCollection subCollection = next.computeIfAbsent(originalRepositoryNameParts.get(0), x -> new LegacyRepositoryInfoCollection()); + subCollection.add(migrationInfo, removeFirstElement(originalRepositoryNameParts)); + } + } + } + + private static List removeFirstElement(List originalRepositoryNameParts) { + return originalRepositoryNameParts.subList(1, originalRepositoryNameParts.size()); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java new file mode 100644 index 0000000000..2dba66f54a --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java @@ -0,0 +1,83 @@ +package sonia.scm.legacy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.migration.MigrationDAO; +import sonia.scm.migration.MigrationInfo; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static java.util.Collections.singletonList; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryLegacyProtocolRedirectFilterTest { + + @Mock + MigrationDAO migrationDAO; + @Mock + HttpServletRequest request; + @Mock + HttpServletResponse response; + @Mock + FilterChain filterChain; + + @BeforeEach + void initContextPath() { + lenient().when(request.getContextPath()).thenReturn("/scm"); + lenient().when(request.getQueryString()).thenReturn(""); + } + + @Test + void shouldNotRedirectForEmptyMigrationList() throws IOException, ServletException { + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO).doFilter(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + } + + @Test + void shouldRedirectForExistingRepository() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void shouldRedirectForExistingRepositoryWithFurtherPathElements() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name/info/refs"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void shouldRedirectWithQueryParameters() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); + when(request.getQueryString()).thenReturn("parameter=value"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO).doFilter(request, response, filterChain); + + verify(response).sendRedirect("/scm/repo/namespace/name/info/refs?parameter=value"); + verify(filterChain, never()).doFilter(request, response); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java index 7b8af8fbcc..7ece64f719 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java @@ -57,6 +57,7 @@ import sonia.scm.group.GroupDisplayManager; import sonia.scm.group.GroupManager; import sonia.scm.group.GroupManagerProvider; import sonia.scm.group.xml.XmlGroupDAO; +import sonia.scm.migration.MigrationDAO; import sonia.scm.net.SSLContextProvider; import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.net.ahc.ContentTransformer; @@ -97,6 +98,7 @@ import sonia.scm.template.MustacheTemplateEngine; import sonia.scm.template.TemplateEngine; import sonia.scm.template.TemplateEngineFactory; import sonia.scm.template.TemplateServlet; +import sonia.scm.update.repository.DefaultMigrationStrategyDAO; import sonia.scm.user.DefaultUserDisplayManager; import sonia.scm.user.DefaultUserManager; import sonia.scm.user.UserDAO; @@ -183,6 +185,7 @@ class ScmServletModule extends ServletModule { bind(RepositoryDAO.class, XmlRepositoryDAO.class); bind(RepositoryRoleDAO.class, XmlRepositoryRoleDAO.class); bind(RepositoryRoleManager.class).to(DefaultRepositoryRoleManager.class); + bind(MigrationDAO.class).to(DefaultMigrationStrategyDAO.class); bindDecorated(RepositoryManager.class, DefaultRepositoryManager.class, RepositoryManagerProvider.class); diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index 445a99feb7..e913cead25 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -10,7 +10,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.lifecycle.RestartEvent; import sonia.scm.event.ScmEventBus; import sonia.scm.update.repository.MigrationStrategy; -import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.DefaultMigrationStrategyDAO; import sonia.scm.update.repository.V1Repository; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; import sonia.scm.util.ValidationUtil; @@ -37,10 +37,10 @@ class MigrationWizardServlet extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(MigrationWizardServlet.class); private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep; - private final MigrationStrategyDao migrationStrategyDao; + private final DefaultMigrationStrategyDAO migrationStrategyDao; @Inject - MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, MigrationStrategyDao migrationStrategyDao) { + MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, DefaultMigrationStrategyDAO migrationStrategyDao) { this.repositoryV1UpdateStep = repositoryV1UpdateStep; this.migrationStrategyDao = migrationStrategyDao; } @@ -103,11 +103,12 @@ class MigrationWizardServlet extends HttpServlet { .forEach( entry-> { String id = entry.getId(); + String protocol = entry.getType(); String originalName = entry.getOriginalName(); String strategy = req.getParameter("strategy-" + id); String namespace = req.getParameter("namespace-" + id); String name = req.getParameter("name-" + id); - migrationStrategyDao.set(id, originalName, MigrationStrategy.valueOf(strategy), namespace, name); + migrationStrategyDao.set(id, protocol, originalName, MigrationStrategy.valueOf(strategy), namespace, name); } ); diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/DefaultMigrationStrategyDAO.java b/scm-webapp/src/main/java/sonia/scm/update/repository/DefaultMigrationStrategyDAO.java new file mode 100644 index 0000000000..4b10c3e6a7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/DefaultMigrationStrategyDAO.java @@ -0,0 +1,44 @@ +package sonia.scm.update.repository; + +import sonia.scm.migration.MigrationDAO; +import sonia.scm.migration.MigrationInfo; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Collection; +import java.util.Optional; + +import static java.util.stream.Collectors.toList; + +@Singleton +public class DefaultMigrationStrategyDAO implements MigrationDAO { + + private final RepositoryMigrationPlan plan; + private final ConfigurationStore store; + + @Inject + public DefaultMigrationStrategyDAO(ConfigurationStoreFactory storeFactory) { + store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build(); + this.plan = store.getOptional().orElse(new RepositoryMigrationPlan()); + } + + public Optional get(String id) { + return plan.get(id); + } + + public void set(String repositoryId, String protocol, String originalName, MigrationStrategy strategy, String newNamespace, String newName) { + plan.set(repositoryId, protocol, originalName, strategy, newNamespace, newName); + store.set(plan); + } + + @Override + public Collection getAll() { + return plan + .getEntries() + .stream() + .map(e -> new MigrationInfo(e.getRepositoryId(), e.getProtocol(), e.getOriginalName(), e.getNewNamespace(), e.getNewName())) + .collect(toList()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java b/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java deleted file mode 100644 index bdc8f97359..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/MigrationStrategyDao.java +++ /dev/null @@ -1,30 +0,0 @@ -package sonia.scm.update.repository; - -import sonia.scm.store.ConfigurationStore; -import sonia.scm.store.ConfigurationStoreFactory; - -import javax.inject.Inject; -import javax.inject.Singleton; -import java.util.Optional; - -@Singleton -public class MigrationStrategyDao { - - private final RepositoryMigrationPlan plan; - private final ConfigurationStore store; - - @Inject - public MigrationStrategyDao(ConfigurationStoreFactory storeFactory) { - store = storeFactory.withType(RepositoryMigrationPlan.class).withName("migration-plan").build(); - this.plan = store.getOptional().orElse(new RepositoryMigrationPlan()); - } - - public Optional get(String id) { - return plan.get(id); - } - - public void set(String repositoryId, String originalName, MigrationStrategy strategy, String newNamespace, String newName) { - plan.set(repositoryId, originalName, strategy, newNamespace, newName); - store.set(plan); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java index 5f523ee76a..81d4d532ec 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/RepositoryMigrationPlan.java @@ -4,6 +4,8 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -29,14 +31,18 @@ class RepositoryMigrationPlan { .findFirst(); } - public void set(String repositoryId, String originalName, MigrationStrategy strategy, String newNamespace, String newName) { + public Collection getEntries() { + return Collections.unmodifiableList(entries); + } + + public void set(String repositoryId, String protocol, String originalName, MigrationStrategy strategy, String newNamespace, String newName) { Optional entry = get(repositoryId); if (entry.isPresent()) { entry.get().setStrategy(strategy); entry.get().setNewNamespace(newNamespace); entry.get().setNewName(newName); } else { - entries.add(new RepositoryMigrationEntry(repositoryId, originalName, strategy, newNamespace, newName)); + entries.add(new RepositoryMigrationEntry(repositoryId, protocol, originalName, strategy, newNamespace, newName)); } } @@ -45,6 +51,7 @@ class RepositoryMigrationPlan { static class RepositoryMigrationEntry { private String repositoryId; + private String protocol; private String originalName; private MigrationStrategy dataMigrationStrategy; private String newNamespace; @@ -53,14 +60,23 @@ class RepositoryMigrationPlan { RepositoryMigrationEntry() { } - RepositoryMigrationEntry(String repositoryId, String originalName, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) { + RepositoryMigrationEntry(String repositoryId, String protocol, String originalName, MigrationStrategy dataMigrationStrategy, String newNamespace, String newName) { this.repositoryId = repositoryId; + this.protocol = protocol; this.originalName = originalName; this.dataMigrationStrategy = dataMigrationStrategy; this.newNamespace = newNamespace; this.newName = newName; } + public String getRepositoryId() { + return repositoryId; + } + + public String getProtocol() { + return protocol; + } + public String getOriginalName() { return originalName; } diff --git a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java index f7a4e1ed37..a2f7656498 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStep.java @@ -49,7 +49,7 @@ import static sonia.scm.version.Version.parse; *
  • a new entry in the new repository-paths.xml database is written,
  • *
  • the data directory is moved or copied to a SCM v2 consistent directory. How this is done * can be specified by a strategy (@see {@link MigrationStrategy}), that has to be set in - * a database file named migration-plan.xml
  • (to create this file, use {@link MigrationStrategyDao}), + * a database file named migration-plan.xml (to create this file, use {@link DefaultMigrationStrategyDAO}), * and *
  • the new metadata.xml file is created.
  • * @@ -63,7 +63,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { private final SCMContextProvider contextProvider; private final XmlRepositoryDAO repositoryDao; - private final MigrationStrategyDao migrationStrategyDao; + private final DefaultMigrationStrategyDAO migrationStrategyDao; private final Injector injector; private final ConfigurationEntryStore propertyStore; @@ -71,7 +71,7 @@ public class XmlRepositoryV1UpdateStep implements CoreUpdateStep { public XmlRepositoryV1UpdateStep( SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDao, - MigrationStrategyDao migrationStrategyDao, + DefaultMigrationStrategyDAO migrationStrategyDao, Injector injector, ConfigurationEntryStoreFactory configurationEntryStoreFactory ) { diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java index 325dc2eb1c..9dbe00059e 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.update.repository.MigrationStrategy; -import sonia.scm.update.repository.MigrationStrategyDao; +import sonia.scm.update.repository.DefaultMigrationStrategyDAO; import sonia.scm.update.repository.V1Repository; import sonia.scm.update.repository.XmlRepositoryV1UpdateStep; @@ -26,7 +26,7 @@ class MigrationWizardServletTest { @Mock XmlRepositoryV1UpdateStep updateStep; @Mock - MigrationStrategyDao migrationStrategyDao; + DefaultMigrationStrategyDAO migrationStrategyDao; @Mock HttpServletRequest request; @@ -233,6 +233,6 @@ class MigrationWizardServletTest { servlet.doPost(request, response); - verify(migrationStrategyDao).set("id", "name", MigrationStrategy.COPY, "namespace", "name"); + verify(migrationStrategyDao).set("id", "git", "name", MigrationStrategy.COPY, "namespace", "name"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java similarity index 83% rename from scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java rename to scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java index d3b487e916..a97d3b077a 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/DefaultMigrationStrategyDAOTest.java @@ -12,7 +12,6 @@ import sonia.scm.SCMContextProvider; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; -import javax.xml.bind.JAXBException; import java.nio.file.Path; import java.util.Optional; @@ -21,7 +20,7 @@ import static sonia.scm.update.repository.MigrationStrategy.INLINE; @ExtendWith(MockitoExtension.class) @ExtendWith(TempDirectory.class) -class MigrationStrategyDaoTest { +class DefaultMigrationStrategyDAOTest { @Mock SCMContextProvider contextProvider; @@ -36,7 +35,7 @@ class MigrationStrategyDaoTest { @Test void shouldReturnEmptyOptionalWhenStoreIsEmpty() { - MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + DefaultMigrationStrategyDAO dao = new DefaultMigrationStrategyDAO(storeFactory); Optional entry = dao.get("any"); @@ -45,9 +44,9 @@ class MigrationStrategyDaoTest { @Test void shouldReturnNewValue() { - MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + DefaultMigrationStrategyDAO dao = new DefaultMigrationStrategyDAO(storeFactory); - dao.set("id", "originalName", INLINE, "space", "name"); + dao.set("id", "git", "originalName", INLINE, "space", "name"); Optional entry = dao.get("id"); @@ -66,14 +65,14 @@ class MigrationStrategyDaoTest { class WithExistingDatabase { @BeforeEach void initExistingDatabase() { - MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + DefaultMigrationStrategyDAO dao = new DefaultMigrationStrategyDAO(storeFactory); - dao.set("id", "originalName", INLINE, "space", "name"); + dao.set("id", "git", "originalName", INLINE, "space", "name"); } @Test void shouldFindExistingValue() { - MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory); + DefaultMigrationStrategyDAO dao = new DefaultMigrationStrategyDAO(storeFactory); Optional entry = dao.get("id"); diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java index f4abb32698..b3be09a9a9 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java @@ -16,8 +16,6 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.store.ConfigurationEntryStore; -import sonia.scm.store.ConfigurationEntryStoreFactory; -import sonia.scm.store.InMemoryConfigurationEntryStore; import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.update.UpdateStepTestUtil; @@ -49,7 +47,7 @@ class XmlRepositoryV1UpdateStepTest { @Mock XmlRepositoryDAO repositoryDAO; @Mock - MigrationStrategyDao migrationStrategyDao; + DefaultMigrationStrategyDAO migrationStrategyDao; InMemoryConfigurationEntryStoreFactory configurationEntryStoreFactory = new InMemoryConfigurationEntryStoreFactory(); @@ -91,7 +89,7 @@ class XmlRepositoryV1UpdateStepTest { void createMigrationPlan() { Answer planAnswer = invocation -> { String id = invocation.getArgument(0).toString(); - return of(new RepositoryMigrationPlan.RepositoryMigrationEntry(id, "originalName", MOVE, "namespace-" + id, "name-" + id)); + return of(new RepositoryMigrationPlan.RepositoryMigrationEntry(id, "git", "originalName", MOVE, "namespace-" + id, "name-" + id)); }; lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenAnswer(planAnswer); From c9861f6a04312a846bd4483de1ff95f5f1bc0232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 2 Jul 2019 11:14:44 +0200 Subject: [PATCH 014/106] Refactor code for readability --- ...epositoryLegacyProtocolRedirectFilter.java | 85 ++++++++++++++----- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java index c008caf111..43808a7fb8 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java @@ -20,6 +20,8 @@ import java.util.Optional; import static java.util.Arrays.asList; import static java.util.Optional.empty; import static java.util.Optional.of; +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.isNotEmpty; @Singleton public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { @@ -37,27 +39,69 @@ public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - String servletPath = request.getServletPath(); - if (servletPath.startsWith("/")) { - servletPath = servletPath.substring(1); + new Worker(request, response, chain).doFilter(); + } + + private class Worker { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final FilterChain chain; + + private Worker(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + this.request = request; + this.response = response; + this.chain = chain; } - String[] pathElements = servletPath.split("/"); - if (pathElements.length > 0) { - Optional repository = info.findRepository(asList(pathElements)); - if (repository.isPresent()) { - String furtherPath = servletPath.substring(repository.get().getProtocol().length() + 1 + repository.get().getOriginalRepositoryName().length()); - String queryString = request.getQueryString(); - if (queryString != null && queryString.length() > 0) { - response.sendRedirect(String.format("%s/repo/%s/%s%s?%s", request.getContextPath(), repository.get().getNamespace(), repository.get().getName(), furtherPath, queryString)); - } else { - response.sendRedirect(String.format("%s/repo/%s/%s%s", request.getContextPath(), repository.get().getNamespace(), repository.get().getName(), furtherPath)); - } + + public void doFilter() throws IOException, ServletException { + String servletPath = getServletPathWithoutLeadingSlash(); + String[] pathElements = servletPath.split("/"); + if (pathElements.length > 0) { + checkPathElements(servletPath, pathElements); } else { - chain.doFilter(request, response); + noRedirect(); } - } else { + } + + private void checkPathElements(String servletPath, String[] pathElements) throws IOException, ServletException { + Optional migrationInfo = info.findRepository(asList(pathElements)); + if (migrationInfo.isPresent()) { + doRedirect(servletPath, migrationInfo.get()); + } else { + noRedirect(); + } + } + + private void doRedirect(String servletPath, MigrationInfo migrationInfo) throws IOException { + String furtherPath = servletPath.substring(migrationInfo.getProtocol().length() + 1 + migrationInfo.getOriginalRepositoryName().length()); + String queryString = request.getQueryString(); + if (isEmpty(queryString)) { + redirectWithoutQueryParameters(migrationInfo, furtherPath); + } else { + redirectWithQueryParameters(migrationInfo, furtherPath, queryString); + } + } + + private void redirectWithoutQueryParameters(MigrationInfo migrationInfo, String furtherPath) throws IOException { + response.sendRedirect(String.format("%s/repo/%s/%s%s", request.getContextPath(), migrationInfo.getNamespace(), migrationInfo.getName(), furtherPath)); + } + + private void redirectWithQueryParameters(MigrationInfo migrationInfo, String furtherPath, String queryString) throws IOException { + response.sendRedirect(String.format("%s/repo/%s/%s%s?%s", request.getContextPath(), migrationInfo.getNamespace(), migrationInfo.getName(), furtherPath, queryString)); + } + + private void noRedirect() throws IOException, ServletException { chain.doFilter(request, response); } + + private String getServletPathWithoutLeadingSlash() { + String servletPath = request.getServletPath(); + if (servletPath.startsWith("/")) { + return servletPath.substring(1); + } else { + return servletPath; + } + } } private static class ProtocolBasedLegacyRepositoryInfo { @@ -93,10 +137,11 @@ public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { private final Map next = new HashMap<>(); public Optional findRepository(List pathElements) { - if (repositories.containsKey(pathElements.get(0))) { - return of(repositories.get(pathElements.get(0))); - } else if (next.containsKey(pathElements.get(0))) { - return next.get(pathElements.get(0)).findRepository(removeFirstElement(pathElements)); + String firstPathElement = pathElements.get(0); + if (repositories.containsKey(firstPathElement)) { + return of(repositories.get(firstPathElement)); + } else if (next.containsKey(firstPathElement)) { + return next.get(firstPathElement).findRepository(removeFirstElement(pathElements)); } else { return empty(); } From 3e653713c156e7a8c8a6e6b2b675a8cdb184d162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 2 Jul 2019 11:28:27 +0200 Subject: [PATCH 015/106] Do not redirect for deleted repositories --- ...epositoryLegacyProtocolRedirectFilter.java | 30 +++++++++++-------- ...itoryLegacyProtocolRedirectFilterTest.java | 30 +++++++++++++++---- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java index 43808a7fb8..b95d374245 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java @@ -2,6 +2,7 @@ package sonia.scm.legacy; import sonia.scm.migration.MigrationDAO; import sonia.scm.migration.MigrationInfo; +import sonia.scm.repository.RepositoryManager; import sonia.scm.web.filter.HttpFilter; import javax.inject.Inject; @@ -21,16 +22,17 @@ import static java.util.Arrays.asList; import static java.util.Optional.empty; import static java.util.Optional.of; import static org.apache.commons.lang.StringUtils.isEmpty; -import static org.apache.commons.lang.StringUtils.isNotEmpty; @Singleton public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { private final ProtocolBasedLegacyRepositoryInfo info; + private final RepositoryManager repositoryManager; @Inject - public RepositoryLegacyProtocolRedirectFilter(MigrationDAO migrationDAO) { + public RepositoryLegacyProtocolRedirectFilter(MigrationDAO migrationDAO, RepositoryManager repositoryManager) { this.info = load(migrationDAO); + this.repositoryManager = repositoryManager; } private static ProtocolBasedLegacyRepositoryInfo load(MigrationDAO migrationDAO) { @@ -53,7 +55,7 @@ public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { this.chain = chain; } - public void doFilter() throws IOException, ServletException { + void doFilter() throws IOException, ServletException { String servletPath = getServletPathWithoutLeadingSlash(); String[] pathElements = servletPath.split("/"); if (pathElements.length > 0) { @@ -72,13 +74,17 @@ public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { } } - private void doRedirect(String servletPath, MigrationInfo migrationInfo) throws IOException { - String furtherPath = servletPath.substring(migrationInfo.getProtocol().length() + 1 + migrationInfo.getOriginalRepositoryName().length()); - String queryString = request.getQueryString(); - if (isEmpty(queryString)) { - redirectWithoutQueryParameters(migrationInfo, furtherPath); + private void doRedirect(String servletPath, MigrationInfo migrationInfo) throws IOException, ServletException { + if (repositoryManager.get(migrationInfo.getId()) == null) { + noRedirect(); } else { - redirectWithQueryParameters(migrationInfo, furtherPath, queryString); + String furtherPath = servletPath.substring(migrationInfo.getProtocol().length() + 1 + migrationInfo.getOriginalRepositoryName().length()); + String queryString = request.getQueryString(); + if (isEmpty(queryString)) { + redirectWithoutQueryParameters(migrationInfo, furtherPath); + } else { + redirectWithQueryParameters(migrationInfo, furtherPath, queryString); + } } } @@ -108,7 +114,7 @@ public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { private final Map infosForProtocol = new HashMap<>(); - public ProtocolBasedLegacyRepositoryInfo(Collection all) { + ProtocolBasedLegacyRepositoryInfo(Collection all) { all.forEach(this::add); } @@ -126,7 +132,7 @@ public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { return infosForProtocol.get(protocol).findRepository(removeFirstElement(pathElements)); } - public boolean isProtocol(String protocol) { + boolean isProtocol(String protocol) { return infosForProtocol.containsKey(protocol); } } @@ -136,7 +142,7 @@ public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { private final Map repositories = new HashMap<>(); private final Map next = new HashMap<>(); - public Optional findRepository(List pathElements) { + Optional findRepository(List pathElements) { String firstPathElement = pathElements.get(0); if (repositories.containsKey(firstPathElement)) { return of(repositories.get(firstPathElement)); diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java index 2dba66f54a..c54193f455 100644 --- a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java @@ -7,6 +7,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.migration.MigrationDAO; import sonia.scm.migration.MigrationInfo; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -15,6 +17,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -26,6 +29,8 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Mock MigrationDAO migrationDAO; @Mock + RepositoryManager repositoryManager; + @Mock HttpServletRequest request; @Mock HttpServletResponse response; @@ -33,7 +38,7 @@ class RepositoryLegacyProtocolRedirectFilterTest { FilterChain filterChain; @BeforeEach - void initContextPath() { + void initRequest() { lenient().when(request.getContextPath()).thenReturn("/scm"); lenient().when(request.getQueryString()).thenReturn(""); } @@ -42,7 +47,7 @@ class RepositoryLegacyProtocolRedirectFilterTest { void shouldNotRedirectForEmptyMigrationList() throws IOException, ServletException { when(request.getServletPath()).thenReturn("/git/old/name"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); verify(filterChain).doFilter(request, response); } @@ -50,9 +55,10 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Test void shouldRedirectForExistingRepository() throws IOException, ServletException { when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryManager.get("id")).thenReturn(new Repository()); when(request.getServletPath()).thenReturn("/git/old/name"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); verify(response).sendRedirect("/scm/repo/namespace/name"); verify(filterChain, never()).doFilter(request, response); @@ -61,9 +67,10 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Test void shouldRedirectForExistingRepositoryWithFurtherPathElements() throws IOException, ServletException { when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryManager.get("id")).thenReturn(new Repository()); when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); verify(response).sendRedirect("/scm/repo/namespace/name/info/refs"); verify(filterChain, never()).doFilter(request, response); @@ -72,12 +79,25 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Test void shouldRedirectWithQueryParameters() throws IOException, ServletException { when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryManager.get("id")).thenReturn(new Repository()); when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); when(request.getQueryString()).thenReturn("parameter=value"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); verify(response).sendRedirect("/scm/repo/namespace/name/info/refs?parameter=value"); verify(filterChain, never()).doFilter(request, response); } + + @Test + void shouldNotRedirectWhenRepositoryHasBeenDeleted() throws IOException, ServletException { + when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); + when(repositoryManager.get("id")).thenReturn(null); + when(request.getServletPath()).thenReturn("/git/old/name"); + + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); + + verify(response, never()).sendRedirect(any()); + verify(filterChain).doFilter(request, response); + } } From c805e477cb548b330a1710dce22725c3fa258323 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 2 Jul 2019 11:57:28 +0200 Subject: [PATCH 016/106] add Logger to print UpdateException-stacktrace on console --- .../src/main/java/sonia/scm/migration/UpdateException.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/migration/UpdateException.java b/scm-core/src/main/java/sonia/scm/migration/UpdateException.java index 4023620b96..946db59536 100644 --- a/scm-core/src/main/java/sonia/scm/migration/UpdateException.java +++ b/scm-core/src/main/java/sonia/scm/migration/UpdateException.java @@ -1,11 +1,17 @@ package sonia.scm.migration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class UpdateException extends RuntimeException { + private static Logger LOG = LoggerFactory.getLogger(UpdateException.class); + public UpdateException(String message) { super(message); } public UpdateException(String message, Throwable cause) { super(message, cause); + LOG.error(message, cause); } } From 23c8ff923efb961ddc4e244b28c66fb3d6be1261 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 2 Jul 2019 15:36:38 +0200 Subject: [PATCH 017/106] exported autocompletes --- scm-ui-components/packages/ui-components/src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 954b0b0955..b4f78c8cf0 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -25,6 +25,8 @@ export { default as Tooltip } from "./Tooltip"; // TODO do we need this? getPageFromMatch is already exported by urls export { getPageFromMatch } from "./urls"; export { default as Autocomplete} from "./Autocomplete"; +export { default as GroupAutocomplete} from "./GroupAutocomplete"; +export { default as UserAutocomplete} from "./UserAutocomplete"; export { default as BranchSelector } from "./BranchSelector"; export { default as MarkdownView } from "./MarkdownView"; export { default as SyntaxHighlighter } from "./SyntaxHighlighter"; From c35363b79ffc773e574c26e47a6c20d5bf82477a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 3 Jul 2019 07:48:06 +0200 Subject: [PATCH 018/106] Store repository id in git config for each repository This is needed after migration from v1 to v2 and is done in GitV1UpdateStep.java. Therefore we hat to make the 'forAllPaths' method in PathBasedRepositoryLocationResolver available in the interface of RepositoryLocationResolver. --- .../RepositoryLocationResolver.java | 9 +++ .../UpdateStepRepositoryMetadataAccess.java | 11 +++ .../scm/repository/xml/MetadataStore.java | 15 ++-- .../PathBasedRepositoryLocationResolver.java | 9 +-- .../xml/SingleRepositoryUpdateProcessor.java | 6 +- .../scm/repository/xml/XmlRepositoryDAO.java | 18 ++--- ...thBasedRepositoryLocationResolverTest.java | 2 +- .../repository/xml/XmlRepositoryDAOTest.java | 22 +++--- .../sonia/scm/repository/GitConfigHelper.java | 21 ++++++ .../scm/repository/GitRepositoryHandler.java | 8 +-- .../repository/update/GitV1UpdateStep.java | 70 +++++++++++++++++++ .../scm/repository/HgRepositoryHandler.java | 10 --- .../TempDirRepositoryLocationResolver.java | 6 ++ .../lifecycle/modules/BootstrapModule.java | 3 + 14 files changed, 158 insertions(+), 52 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV1UpdateStep.java diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java index 1b7da51c4c..bdd7a03d62 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -1,5 +1,7 @@ package sonia.scm.repository; +import java.util.function.BiConsumer; + public abstract class RepositoryLocationResolver { public abstract boolean supportsLocationType(Class type); @@ -35,5 +37,12 @@ public abstract class RepositoryLocationResolver { * @throws IllegalStateException when there already is a location for the given repository registered. */ void setLocation(String repositoryId, T location); + + /** + * Iterates all repository locations known to this resolver instance and calls the consumer giving the repository id + * and its location for each repository. + * @param consumer This callback will be called for each repository with the repository id and its location. + */ + void forAllLocations(BiConsumer consumer); } } diff --git a/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java b/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java new file mode 100644 index 0000000000..33afe9f76b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/UpdateStepRepositoryMetadataAccess.java @@ -0,0 +1,11 @@ +package sonia.scm.update; + +import sonia.scm.repository.Repository; + +/** + * Use this in {@link sonia.scm.migration.UpdateStep}s only to read repository objects directly from locations given by + * {@link sonia.scm.repository.RepositoryLocationResolver}. + */ +public interface UpdateStepRepositoryMetadataAccess { + Repository read(T location); +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java index 1f5f0e81b6..f22180ff9a 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java @@ -5,19 +5,21 @@ import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; +import sonia.scm.store.StoreConstants; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import java.nio.file.Path; -class MetadataStore { +public class MetadataStore implements UpdateStepRepositoryMetadataAccess { private static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class); private final JAXBContext jaxbContext; - MetadataStore() { + public MetadataStore() { try { jaxbContext = JAXBContext.newInstance(Repository.class); } catch (JAXBException ex) { @@ -25,10 +27,10 @@ class MetadataStore { } } - Repository read(Path path) { + public Repository read(Path path) { LOG.trace("read repository metadata from {}", path); try { - return (Repository) jaxbContext.createUnmarshaller().unmarshal(path.toFile()); + return (Repository) jaxbContext.createUnmarshaller().unmarshal(resolveDataPath(path).toFile()); } catch (JAXBException ex) { throw new InternalRepositoryException( ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex @@ -41,10 +43,13 @@ class MetadataStore { try { Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - marshaller.marshal(repository, path.toFile()); + marshaller.marshal(repository, resolveDataPath(path).toFile()); } catch (JAXBException ex) { throw new InternalRepositoryException(repository, "failed write repository metadata", ex); } } + private Path resolveDataPath(Path repositoryPath) { + return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); + } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java index 81cf167071..8f667b0e65 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java @@ -94,6 +94,11 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath()); } } + + @Override + public void forAllLocations(BiConsumer consumer) { + pathById.forEach((id, path) -> consumer.accept(id, (T) contextProvider.resolve(path))); + } }; } @@ -115,10 +120,6 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation return contextProvider.resolve(removedPath); } - void forAllPaths(BiConsumer consumer) { - pathById.forEach((id, path) -> consumer.accept(id, contextProvider.resolve(path))); - } - void updateModificationDate() { this.writePathDatabase(); } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java index eeb95f75b0..f963047624 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/SingleRepositoryUpdateProcessor.java @@ -1,5 +1,7 @@ package sonia.scm.repository.xml; +import sonia.scm.repository.RepositoryLocationResolver; + import javax.inject.Inject; import java.nio.file.Path; import java.util.function.BiConsumer; @@ -7,9 +9,9 @@ import java.util.function.BiConsumer; public class SingleRepositoryUpdateProcessor { @Inject - private PathBasedRepositoryLocationResolver locationResolver; + private RepositoryLocationResolver locationResolver; public void doUpdate(BiConsumer forEachRepository) { - locationResolver.forAllPaths(forEachRepository); + locationResolver.forClass(Path.class).forAllLocations(forEachRepository); } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index 151e8f1281..1242c99641 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -40,7 +40,7 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; -import sonia.scm.store.StoreConstants; +import sonia.scm.repository.RepositoryLocationResolver; import javax.inject.Inject; import java.io.IOException; @@ -76,18 +76,14 @@ public class XmlRepositoryDAO implements RepositoryDAO { } private void init() { - repositoryLocationResolver.forAllPaths((repositoryId, repositoryPath) -> { - Path metadataPath = resolveDataPath(repositoryPath); - Repository repository = metadataStore.read(metadataPath); + RepositoryLocationResolver.RepositoryLocationResolverInstance pathRepositoryLocationResolverInstance = repositoryLocationResolver.create(Path.class); + pathRepositoryLocationResolverInstance.forAllLocations((repositoryId, repositoryPath) -> { + Repository repository = metadataStore.read(repositoryPath); byNamespaceAndName.put(repository.getNamespaceAndName(), repository); byId.put(repositoryId, repository); }); } - private Path resolveDataPath(Path repositoryPath) { - return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); - } - @Override public String getType() { return "xml"; @@ -108,8 +104,7 @@ public class XmlRepositoryDAO implements RepositoryDAO { Path repositoryPath = (Path) location; try { - Path metadataPath = resolveDataPath(repositoryPath); - metadataStore.write(metadataPath, repository); + metadataStore.write(repositoryPath, repository); } catch (Exception e) { repositoryLocationResolver.remove(repository.getId()); throw new InternalRepositoryException(repository, "failed to create filesystem", e); @@ -166,9 +161,8 @@ public class XmlRepositoryDAO implements RepositoryDAO { Path repositoryPath = repositoryLocationResolver .create(Path.class) .getLocation(repository.getId()); - Path metadataPath = resolveDataPath(repositoryPath); repositoryLocationResolver.updateModificationDate(); - metadataStore.write(metadataPath, clone); + metadataStore.write(repositoryPath, clone); } @Override diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java index 941775d6ea..1c5a337abc 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java @@ -120,7 +120,7 @@ class PathBasedRepositoryLocationResolverTest { @Test void shouldInitWithExistingData() { Map foundRepositories = new HashMap<>(); - resolverWithExistingData.forAllPaths( + resolverWithExistingData.forClass(Path.class).forAllLocations( foundRepositories::put ); assertThat(foundRepositories) diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index bdf28310e1..f5571441e7 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -26,15 +26,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.function.BiConsumer; +import java.util.function.Consumer; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -47,6 +45,7 @@ class XmlRepositoryDAOTest { @Mock private PathBasedRepositoryLocationResolver locationResolver; + private Consumer> triggeredOnForAllLocations = none -> {}; private FileSystem fileSystem = new DefaultFileSystem(); @@ -69,6 +68,11 @@ class XmlRepositoryDAOTest { @Override public void setLocation(String repositoryId, Path location) { } + + @Override + public void forAllLocations(BiConsumer consumer) { + triggeredOnForAllLocations.accept(consumer); + } } ); when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation)); @@ -332,11 +336,10 @@ class XmlRepositoryDAOTest { @Test void shouldRefreshWithExistingRepositoriesFromPathDatabase() { // given - doNothing().when(locationResolver).forAllPaths(any()); - XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); - mockExistingPath(); + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + // when dao.refresh(); @@ -346,12 +349,7 @@ class XmlRepositoryDAOTest { } private void mockExistingPath() { - doAnswer( - invocation -> { - ((BiConsumer) invocation.getArgument(0)).accept("existing", repositoryPath); - return null; - } - ).when(locationResolver).forAllPaths(any()); + triggeredOnForAllLocations = consumer -> consumer.accept("existing", repositoryPath); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java new file mode 100644 index 0000000000..a978295166 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfigHelper.java @@ -0,0 +1,21 @@ +package sonia.scm.repository; + +import org.eclipse.jgit.lib.StoredConfig; + +import java.io.IOException; + +public class GitConfigHelper { + + private static final String CONFIG_SECTION_SCMM = "scmm"; + private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; + + public void createScmmConfig(Repository repository, org.eclipse.jgit.lib.Repository gitRepository) throws IOException { + StoredConfig config = gitRepository.getConfig(); + config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId()); + config.save(); + } + + public String getRepositoryId(StoredConfig gitConfig) { + return gitConfig.getString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 63800e8a02..e07b3d0d83 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -89,8 +89,6 @@ public class GitRepositoryHandler GitRepositoryServiceProvider.COMMANDS); private static final Object LOCK = new Object(); - private static final String CONFIG_SECTION_SCMM = "scmm"; - private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; private final Scheduler scheduler; @@ -185,7 +183,7 @@ public class GitRepositoryHandler } public String getRepositoryId(StoredConfig gitConfig) { - return gitConfig.getString(GitRepositoryHandler.CONFIG_SECTION_SCMM, null, GitRepositoryHandler.CONFIG_KEY_REPOSITORY_ID); + return new GitConfigHelper().getRepositoryId(gitConfig); } //~--- methods -------------------------------------------------------------- @@ -194,9 +192,7 @@ public class GitRepositoryHandler protected void create(Repository repository, File directory) throws IOException { try (org.eclipse.jgit.lib.Repository gitRepository = build(directory)) { gitRepository.create(true); - StoredConfig config = gitRepository.getConfig(); - config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId()); - config.save(); + new GitConfigHelper().createScmmConfig(repository, gitRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV1UpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV1UpdateStep.java new file mode 100644 index 0000000000..7cab0765a7 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV1UpdateStep.java @@ -0,0 +1,70 @@ +package sonia.scm.repository.update; + +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.GitConfigHelper; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static sonia.scm.version.Version.parse; + +@Extension +public class GitV1UpdateStep implements UpdateStep { + + private final RepositoryLocationResolver locationResolver; + private final UpdateStepRepositoryMetadataAccess repositoryMetadataAccess; + + @Inject + public GitV1UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { + this.locationResolver = locationResolver; + this.repositoryMetadataAccess = repositoryMetadataAccess; + } + + @Override + public void doUpdate() { + locationResolver.forClass(Path.class).forAllLocations( + (repositoryId, path) -> { + Repository repository = repositoryMetadataAccess.read(path); + if (isGitDirectory(repository)) { + try (org.eclipse.jgit.lib.Repository gitRepository = build(path.resolve("data").toFile())) { + new GitConfigHelper().createScmmConfig(repository, gitRepository); + } catch (IOException e) { + throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e); + } + } + } + ); + } + + private org.eclipse.jgit.lib.Repository build(File directory) throws IOException { + return new FileRepositoryBuilder() + .setGitDir(directory) + .readEnvironment() + .findGitDir() + .build(); + } + + private boolean isGitDirectory(Repository repository) { + return GitRepositoryHandler.TYPE_NAME.equals(repository.getType()); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.plugin.git"; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index 4ccf13e738..0a8dfe065a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -41,13 +41,11 @@ import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ConfigurationException; -import sonia.scm.ContextEntry; import sonia.scm.SCMContextProvider; import sonia.scm.installer.HgInstaller; import sonia.scm.installer.HgInstallerFactory; import sonia.scm.io.ExtendedCommand; import sonia.scm.io.INIConfiguration; -import sonia.scm.io.INIConfigurationReader; import sonia.scm.io.INIConfigurationWriter; import sonia.scm.io.INISection; import sonia.scm.plugin.Extension; @@ -347,14 +345,6 @@ public class HgRepositoryHandler writer.write(hgrc, hgrcFile); } - public String getRepositoryId(File directory) { - try { - return new INIConfigurationReader().read(new File(directory, PATH_HGRC)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID); - } catch (IOException e) { - throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e); - } - } - //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java index acffe6c769..e172b53ba2 100644 --- a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java +++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java @@ -4,6 +4,7 @@ import sonia.scm.repository.BasicRepositoryLocationResolver; import java.io.File; import java.nio.file.Path; +import java.util.function.BiConsumer; public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationResolver { private final File tempDirectory; @@ -30,6 +31,11 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe public void setLocation(String repositoryId, T location) { throw new UnsupportedOperationException("not implemented for tests"); } + + @Override + public void forAllLocations(BiConsumer consumer) { + consumer.accept("id", (T) tempDirectory.toPath()); + } }; } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java index 9b6ea719d8..fc8101d7cb 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java @@ -10,6 +10,7 @@ import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.xml.MetadataStore; import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; import sonia.scm.security.CipherHandler; import sonia.scm.security.CipherUtil; @@ -25,6 +26,7 @@ import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.store.JAXBDataStoreFactory; import sonia.scm.store.JAXBPropertyFileAccess; import sonia.scm.update.PropertyFileAccess; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; import sonia.scm.update.V1PropertyDAO; import sonia.scm.update.xml.XmlV1PropertyDAO; @@ -65,6 +67,7 @@ public class BootstrapModule extends AbstractModule { bind(PluginLoader.class).toInstance(pluginLoader); bind(V1PropertyDAO.class, XmlV1PropertyDAO.class); bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class); + bind(UpdateStepRepositoryMetadataAccess.class).to(MetadataStore.class); } private void bind(Class clazz, Class defaultImplementation) { From dc7407d6e5376101f87bb5c2ed622d1a57e9d2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 3 Jul 2019 07:58:38 +0200 Subject: [PATCH 019/106] Fix guice binding --- .../java/sonia/scm/lifecycle/modules/BootstrapModule.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java index fc8101d7cb..26d70c465b 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java @@ -1,6 +1,7 @@ package sonia.scm.lifecycle.modules; import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; import com.google.inject.throwingproviders.ThrowingProviderBinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,6 +31,8 @@ import sonia.scm.update.UpdateStepRepositoryMetadataAccess; import sonia.scm.update.V1PropertyDAO; import sonia.scm.update.xml.XmlV1PropertyDAO; +import java.nio.file.Path; + public class BootstrapModule extends AbstractModule { private static final Logger LOG = LoggerFactory.getLogger(BootstrapModule.class); @@ -67,7 +70,7 @@ public class BootstrapModule extends AbstractModule { bind(PluginLoader.class).toInstance(pluginLoader); bind(V1PropertyDAO.class, XmlV1PropertyDAO.class); bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class); - bind(UpdateStepRepositoryMetadataAccess.class).to(MetadataStore.class); + bind(new TypeLiteral>() {}).to(new TypeLiteral() {}); } private void bind(Class clazz, Class defaultImplementation) { From ab62b121d7d071e71c86ae1f7f2ebc53105be37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 3 Jul 2019 08:16:08 +0200 Subject: [PATCH 020/106] Store repository id in svn config for each repository This is needed after migration from v1 to v2 and is done in SvnV1UpdateStep.java. --- .../sonia/scm/repository/SvnConfigHelper.java | 33 +++++++++++ .../scm/repository/SvnRepositoryHandler.java | 20 +------ .../sonia/scm/repository/SvnV1UpdateStep.java | 56 +++++++++++++++++++ 3 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java create mode 100644 scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java new file mode 100644 index 0000000000..ab01393b29 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfigHelper.java @@ -0,0 +1,33 @@ +package sonia.scm.repository; + +import sonia.scm.ContextEntry; +import sonia.scm.io.INIConfiguration; +import sonia.scm.io.INIConfigurationReader; +import sonia.scm.io.INIConfigurationWriter; +import sonia.scm.io.INISection; + +import java.io.File; +import java.io.IOException; + +class SvnConfigHelper { + + private static final String CONFIG_FILE_NAME = "scm-manager.conf"; + private static final String CONFIG_SECTION_SCMM = "scmm"; + private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; + + void writeRepositoryId(Repository repository, File directory) throws IOException { + INISection iniSection = new INISection(CONFIG_SECTION_SCMM); + iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId()); + INIConfiguration iniConfiguration = new INIConfiguration(); + iniConfiguration.addSection(iniSection); + new INIConfigurationWriter().write(iniConfiguration, new File(directory, CONFIG_FILE_NAME)); + } + + String getRepositoryId(File directory) { + try { + return new INIConfigurationReader().read(new File(directory, CONFIG_FILE_NAME)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID); + } catch (IOException e) { + throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index c32310e7e7..f3be47e0c2 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -46,11 +46,6 @@ import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.util.SVNDebugLog; -import sonia.scm.ContextEntry; -import sonia.scm.io.INIConfiguration; -import sonia.scm.io.INIConfigurationReader; -import sonia.scm.io.INIConfigurationWriter; -import sonia.scm.io.INISection; import sonia.scm.logging.SVNKitLogger; import sonia.scm.plugin.Extension; import sonia.scm.plugin.PluginLoader; @@ -87,9 +82,6 @@ public class SvnRepositoryHandler public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, SvnRepositoryServiceProvider.COMMANDS); - private static final String CONFIG_FILE_NAME = "scm-manager.conf"; - private static final String CONFIG_SECTION_SCMM = "scmm"; - private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid"; private static final Logger logger = LoggerFactory.getLogger(SvnRepositoryHandler.class); @@ -223,18 +215,10 @@ public class SvnRepositoryHandler @Override protected void postCreate(Repository repository, File directory) throws IOException { - INISection iniSection = new INISection(CONFIG_SECTION_SCMM); - iniSection.setParameter(CONFIG_KEY_REPOSITORY_ID, repository.getId()); - INIConfiguration iniConfiguration = new INIConfiguration(); - iniConfiguration.addSection(iniSection); - new INIConfigurationWriter().write(iniConfiguration, new File(directory, CONFIG_FILE_NAME)); + new SvnConfigHelper().writeRepositoryId(repository, directory); } String getRepositoryId(File directory) { - try { - return new INIConfigurationReader().read(new File(directory, CONFIG_FILE_NAME)).getSection(CONFIG_SECTION_SCMM).getParameter(CONFIG_KEY_REPOSITORY_ID); - } catch (IOException e) { - throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Directory", directory.toString()), "could not read scm configuration file", e); - } + return new SvnConfigHelper().getRepositoryId(directory); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java new file mode 100644 index 0000000000..fe96a7e945 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java @@ -0,0 +1,56 @@ +package sonia.scm.repository; + +import sonia.scm.migration.UpdateException; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.update.UpdateStepRepositoryMetadataAccess; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Path; + +import static sonia.scm.version.Version.parse; + +@Extension +public class SvnV1UpdateStep implements UpdateStep { + + private final RepositoryLocationResolver locationResolver; + private final UpdateStepRepositoryMetadataAccess repositoryMetadataAccess; + + @Inject + public SvnV1UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { + this.locationResolver = locationResolver; + this.repositoryMetadataAccess = repositoryMetadataAccess; + } + + @Override + public void doUpdate() { + locationResolver.forClass(Path.class).forAllLocations( + (repositoryId, path) -> { + Repository repository = repositoryMetadataAccess.read(path); + if (isSvnDirectory(repository)) { + try { + new SvnConfigHelper().writeRepositoryId(repository, path.toFile()); + } catch (IOException e) { + throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e); + } + } + } + ); + } + + private boolean isSvnDirectory(Repository repository) { + return SvnRepositoryHandler.TYPE_NAME.equals(repository.getType()); + } + + @Override + public Version getTargetVersion() { + return parse("2.0.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.plugin.svn"; + } +} From 226322d05e25d81d284f6ed36f42709b738ceb23 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 3 Jul 2019 12:41:46 +0200 Subject: [PATCH 021/106] Use dao to prevent authentication issues --- ...epositoryLegacyProtocolRedirectFilter.java | 13 ++++++----- ...itoryLegacyProtocolRedirectFilterTest.java | 22 +++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java index b95d374245..6319af04f6 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilter.java @@ -1,8 +1,10 @@ package sonia.scm.legacy; +import sonia.scm.Priority; +import sonia.scm.filter.Filters; import sonia.scm.migration.MigrationDAO; import sonia.scm.migration.MigrationInfo; -import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryDAO; import sonia.scm.web.filter.HttpFilter; import javax.inject.Inject; @@ -23,16 +25,17 @@ import static java.util.Optional.empty; import static java.util.Optional.of; import static org.apache.commons.lang.StringUtils.isEmpty; +@Priority(Filters.PRIORITY_BASEURL) @Singleton public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { private final ProtocolBasedLegacyRepositoryInfo info; - private final RepositoryManager repositoryManager; + private final RepositoryDAO repositoryDao; @Inject - public RepositoryLegacyProtocolRedirectFilter(MigrationDAO migrationDAO, RepositoryManager repositoryManager) { + public RepositoryLegacyProtocolRedirectFilter(MigrationDAO migrationDAO, RepositoryDAO repositoryDao) { this.info = load(migrationDAO); - this.repositoryManager = repositoryManager; + this.repositoryDao = repositoryDao; } private static ProtocolBasedLegacyRepositoryInfo load(MigrationDAO migrationDAO) { @@ -75,7 +78,7 @@ public class RepositoryLegacyProtocolRedirectFilter extends HttpFilter { } private void doRedirect(String servletPath, MigrationInfo migrationInfo) throws IOException, ServletException { - if (repositoryManager.get(migrationInfo.getId()) == null) { + if (repositoryDao.get(migrationInfo.getId()) == null) { noRedirect(); } else { String furtherPath = servletPath.substring(migrationInfo.getProtocol().length() + 1 + migrationInfo.getOriginalRepositoryName().length()); diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java index c54193f455..bd70cb9dfc 100644 --- a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/RepositoryLegacyProtocolRedirectFilterTest.java @@ -8,7 +8,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.migration.MigrationDAO; import sonia.scm.migration.MigrationInfo; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryDAO; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -29,7 +29,7 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Mock MigrationDAO migrationDAO; @Mock - RepositoryManager repositoryManager; + RepositoryDAO repositoryDao; @Mock HttpServletRequest request; @Mock @@ -47,7 +47,7 @@ class RepositoryLegacyProtocolRedirectFilterTest { void shouldNotRedirectForEmptyMigrationList() throws IOException, ServletException { when(request.getServletPath()).thenReturn("/git/old/name"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); verify(filterChain).doFilter(request, response); } @@ -55,10 +55,10 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Test void shouldRedirectForExistingRepository() throws IOException, ServletException { when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); - when(repositoryManager.get("id")).thenReturn(new Repository()); + when(repositoryDao.get("id")).thenReturn(new Repository()); when(request.getServletPath()).thenReturn("/git/old/name"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); verify(response).sendRedirect("/scm/repo/namespace/name"); verify(filterChain, never()).doFilter(request, response); @@ -67,10 +67,10 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Test void shouldRedirectForExistingRepositoryWithFurtherPathElements() throws IOException, ServletException { when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); - when(repositoryManager.get("id")).thenReturn(new Repository()); + when(repositoryDao.get("id")).thenReturn(new Repository()); when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); verify(response).sendRedirect("/scm/repo/namespace/name/info/refs"); verify(filterChain, never()).doFilter(request, response); @@ -79,11 +79,11 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Test void shouldRedirectWithQueryParameters() throws IOException, ServletException { when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); - when(repositoryManager.get("id")).thenReturn(new Repository()); + when(repositoryDao.get("id")).thenReturn(new Repository()); when(request.getServletPath()).thenReturn("/git/old/name/info/refs"); when(request.getQueryString()).thenReturn("parameter=value"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); verify(response).sendRedirect("/scm/repo/namespace/name/info/refs?parameter=value"); verify(filterChain, never()).doFilter(request, response); @@ -92,10 +92,10 @@ class RepositoryLegacyProtocolRedirectFilterTest { @Test void shouldNotRedirectWhenRepositoryHasBeenDeleted() throws IOException, ServletException { when(migrationDAO.getAll()).thenReturn(singletonList(new MigrationInfo("id", "git", "old/name", "namespace", "name"))); - when(repositoryManager.get("id")).thenReturn(null); + when(repositoryDao.get("id")).thenReturn(null); when(request.getServletPath()).thenReturn("/git/old/name"); - new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryManager).doFilter(request, response, filterChain); + new RepositoryLegacyProtocolRedirectFilter(migrationDAO, repositoryDao).doFilter(request, response, filterChain); verify(response, never()).sendRedirect(any()); verify(filterChain).doFilter(request, response); From 8a408c4600abca395bb7410b256ce99da987fe01 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 3 Jul 2019 10:44:10 +0000 Subject: [PATCH 022/106] Close branch bugfix/autocomplete2 From 61b51f0063cec3e030e4b8d84659e863196328e5 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 3 Jul 2019 13:06:11 +0200 Subject: [PATCH 023/106] Fix configuration path --- .../src/main/java/sonia/scm/repository/SvnV1UpdateStep.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java index fe96a7e945..2fadc4a6e1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java @@ -31,7 +31,7 @@ public class SvnV1UpdateStep implements UpdateStep { Repository repository = repositoryMetadataAccess.read(path); if (isSvnDirectory(repository)) { try { - new SvnConfigHelper().writeRepositoryId(repository, path.toFile()); + new SvnConfigHelper().writeRepositoryId(repository, path.resolve(RepositoryDirectoryHandler.REPOSITORIES_NATIVE_DIRECTORY).toFile()); } catch (IOException e) { throw new UpdateException("could not update repository with id " + repositoryId + " in path " + path, e); } From d1b167677b15317374f44ee996796884b68621c4 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 3 Jul 2019 13:21:34 +0200 Subject: [PATCH 024/106] add LegacyPlugin Endpoint to get the repository name + namespace --- .../scm/legacy/LegacyIndexHalEnricher.java | 38 +++++++++++++++++++ .../scm/legacy/LegacyRepositoryService.java | 38 +++++++++++++++++++ .../sonia/scm/legacy/NamespaceAndNameDto.java | 11 ++++++ 3 files changed, 87 insertions(+) create mode 100644 scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java create mode 100644 scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java create mode 100644 scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java new file mode 100644 index 0000000000..0914f92e25 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyIndexHalEnricher.java @@ -0,0 +1,38 @@ +package sonia.scm.legacy; + +import com.google.inject.Inject; +import sonia.scm.api.v2.resources.Enrich; +import sonia.scm.api.v2.resources.HalAppender; +import sonia.scm.api.v2.resources.HalEnricher; +import sonia.scm.api.v2.resources.HalEnricherContext; +import sonia.scm.api.v2.resources.Index; +import sonia.scm.api.v2.resources.LinkBuilder; +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.plugin.Extension; + +import javax.inject.Provider; + +@Extension +@Enrich(Index.class) +public class LegacyIndexHalEnricher implements HalEnricher { + + private Provider scmPathInfoStoreProvider; + + @Inject + public LegacyIndexHalEnricher(Provider scmPathInfoStoreProvider) { + this.scmPathInfoStoreProvider = scmPathInfoStoreProvider; + } + + private String createLink() { + return new LinkBuilder(scmPathInfoStoreProvider.get().get(), LegacyRepositoryService.class) + .method("getNameAndNamespaceForRepositoryId") + .parameters("REPOID") + .href() + .replace("REPOID", "{id}"); + } + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + appender.appendLink("nameAndNamespace", createLink()); + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java new file mode 100644 index 0000000000..69356ca6fa --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java @@ -0,0 +1,38 @@ +package sonia.scm.legacy; + +import com.google.inject.Inject; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +@Path("v2/legacy/repository") +public class LegacyRepositoryService { + + private RepositoryManager repositoryManager; + + @Inject + public LegacyRepositoryService(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + @GET + @Path("{id}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:read:global\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public NamespaceAndNameDto getNameAndNamespaceForRepositoryId(@PathParam("id") String repositoryId) { + Repository repo = repositoryManager.get(repositoryId); + return new NamespaceAndNameDto(repo.getName(), repo.getNamespace()); + + } + +} + diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java new file mode 100644 index 0000000000..2f7aae2dae --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/NamespaceAndNameDto.java @@ -0,0 +1,11 @@ +package sonia.scm.legacy; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class NamespaceAndNameDto { + private String name; + private String namespace; +} From 431902ff91f28002ec543dd602545050529cb7f5 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 3 Jul 2019 13:32:59 +0200 Subject: [PATCH 025/106] Rename classes --- .../update/{GitV1UpdateStep.java => GitV2UpdateStep.java} | 4 ++-- .../repository/{SvnV1UpdateStep.java => SvnV2UpdateStep.java} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/{GitV1UpdateStep.java => GitV2UpdateStep.java} (94%) rename scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/{SvnV1UpdateStep.java => SvnV2UpdateStep.java} (93%) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV1UpdateStep.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java similarity index 94% rename from scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV1UpdateStep.java rename to scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java index 7cab0765a7..afcde567e3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV1UpdateStep.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/update/GitV2UpdateStep.java @@ -19,13 +19,13 @@ import java.nio.file.Path; import static sonia.scm.version.Version.parse; @Extension -public class GitV1UpdateStep implements UpdateStep { +public class GitV2UpdateStep implements UpdateStep { private final RepositoryLocationResolver locationResolver; private final UpdateStepRepositoryMetadataAccess repositoryMetadataAccess; @Inject - public GitV1UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { + public GitV2UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { this.locationResolver = locationResolver; this.repositoryMetadataAccess = repositoryMetadataAccess; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java similarity index 93% rename from scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java rename to scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java index 2fadc4a6e1..2423040f60 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV1UpdateStep.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnV2UpdateStep.java @@ -13,13 +13,13 @@ import java.nio.file.Path; import static sonia.scm.version.Version.parse; @Extension -public class SvnV1UpdateStep implements UpdateStep { +public class SvnV2UpdateStep implements UpdateStep { private final RepositoryLocationResolver locationResolver; private final UpdateStepRepositoryMetadataAccess repositoryMetadataAccess; @Inject - public SvnV1UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { + public SvnV2UpdateStep(RepositoryLocationResolver locationResolver, UpdateStepRepositoryMetadataAccess repositoryMetadataAccess) { this.locationResolver = locationResolver; this.repositoryMetadataAccess = repositoryMetadataAccess; } From 7211d657f2c5c4a7e8ff6c71ff98416a20296cf1 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 3 Jul 2019 15:06:46 +0200 Subject: [PATCH 026/106] call new LegacyRepository-Endpoint on Main.js --- scm-plugins/scm-legacy-plugin/pom.xml | 8 +++++++- .../scm/legacy/LegacyRepositoryService.java | 3 +++ scm-ui/src/containers/Main.js | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/scm-plugins/scm-legacy-plugin/pom.xml b/scm-plugins/scm-legacy-plugin/pom.xml index 52aff51dd2..6cfa74ea61 100644 --- a/scm-plugins/scm-legacy-plugin/pom.xml +++ b/scm-plugins/scm-legacy-plugin/pom.xml @@ -21,7 +21,13 @@ ${servlet.version} provided - + + javax.ws.rs + jsr311-api + 1.1.1 + compile + + diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java index 69356ca6fa..9e27c736ea 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java @@ -9,6 +9,8 @@ import sonia.scm.repository.RepositoryManager; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; @Path("v2/legacy/repository") public class LegacyRepositoryService { @@ -22,6 +24,7 @@ public class LegacyRepositoryService { @GET @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 8681f2457b..129b11f5ba 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -9,7 +9,7 @@ import Users from "../users/containers/Users"; import Login from "../containers/Login"; import Logout from "../containers/Logout"; -import { ProtectedRoute } from "@scm-manager/ui-components"; +import { ProtectedRoute, apiClient } from "@scm-manager/ui-components"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import CreateUser from "../users/containers/CreateUser"; @@ -27,7 +27,10 @@ import Profile from "./Profile"; type Props = { authenticated?: boolean, - links: Links + links: Links, + + //context objects + history: History }; class Main extends React.Component { @@ -35,6 +38,15 @@ class Main extends React.Component { const { authenticated, links } = this.props; const redirectUrlFactory = binder.getExtension("main.redirect", this.props); let url = "/repos"; + if (location.href && location.href.includes("#diffPanel;")) { + let repoId = location.href.substring(location.href.search("#diffPanel;") + 11, location.href.search("#diffPanel;") + 21); + console.log("RepoId:"); + console.log(repoId); + apiClient.get("/legacy/repository/" + repoId).then(response => + console.log(JSON.parse(response)) + // this.props.history.push("/repo/" + response.responseBody.namespace + "/" + response.responseBody.name) + ); + } if (redirectUrlFactory) { url = redirectUrlFactory(this.props); } From c16aeed96dac6ccbc70a92569a60f93b6d609781 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 3 Jul 2019 16:23:27 +0200 Subject: [PATCH 027/106] Check permissions to read permissions --- .../sonia/scm/api/v2/resources/GroupPermissionResource.java | 2 ++ .../java/sonia/scm/api/v2/resources/UserPermissionResource.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java index 11934abcb0..dfce12e778 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; +import sonia.scm.security.PermissionPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -47,6 +48,7 @@ public class GroupPermissionResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getPermissions(@PathParam("id") String id) { + PermissionPermissions.read().check(); Collection permissions = permissionAssigner.readPermissionsForGroup(id); return Response.ok(permissionCollectionToDtoMapper.mapForGroup(permissions, id)).build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java index a961dfaa0e..fd54da503d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; +import sonia.scm.security.PermissionPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -48,6 +49,7 @@ public class UserPermissionResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getPermissions(@PathParam("id") String id) { + PermissionPermissions.read().check(); Collection permissions = permissionAssigner.readPermissionsForUser(id); return Response.ok(permissionCollectionToDtoMapper.mapForUser(permissions, id)).build(); } From ca68b6346e7433f7c01a92e640bd3c352d5a529f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 3 Jul 2019 17:27:59 +0200 Subject: [PATCH 028/106] Double check directory creation --- scm-core/src/main/java/sonia/scm/util/IOUtil.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/util/IOUtil.java b/scm-core/src/main/java/sonia/scm/util/IOUtil.java index d71f8a36fc..16f84d1031 100644 --- a/scm-core/src/main/java/sonia/scm/util/IOUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/IOUtil.java @@ -471,8 +471,14 @@ public final class IOUtil { if (!directory.exists() &&!directory.mkdirs()) { - throw new IllegalStateException( - "could not create directory ".concat(directory.getPath())); + // Sometimes, the previous check simply has the wrong result (either the 'exists()' returnes false though the + // directory exists or 'mkdirs()' returns false though the directory was created successfully. + // We therefore have to double check here. Funny though, in these cases a second check with 'directory.exists()' + // still returns false. As it seems, 'directory.getAbsoluteFile().exists()' creates a new object that fixes this + // problem. + if (!directory.getAbsoluteFile().exists()) { + throw new IllegalStateException("could not create directory ".concat(directory.getPath())); + } } } From 69b26f0f29152bc8c3c6297f24487bdab584e1da Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 4 Jul 2019 11:46:25 +0200 Subject: [PATCH 029/106] Add hint that exception can be ignored --- .../src/main/java/sonia/scm/repository/HgEnvironment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index 51ae6d5786..b8883a0d92 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -118,7 +118,7 @@ public final class HgEnvironment String credentials = hookManager.getCredentials(); environment.put(SCM_BEARER_TOKEN, credentials); } catch (ProvisionException e) { - LOG.debug("could not create bearer token; looks like currently we are not in a request", e); + LOG.debug("could not create bearer token; looks like currently we are not in a request; probably you can ignore the following exception:", e); } environment.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig())); environment.put(ENV_URL, hookUrl); From 4f1ac2af09fb4d898e2ff46525b81cacbb2c6f6c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 4 Jul 2019 12:13:45 +0200 Subject: [PATCH 030/106] move redirect logic to legacy plugin / bind Extensionpoint in Main.js --- scm-plugins/scm-legacy-plugin/package.json | 20 +++++++ .../src/main/js/DummyComponent.js | 15 +++++ .../scm-legacy-plugin/src/main/js/index.js | 55 +++++++++++++++++++ scm-ui/src/containers/Main.js | 15 ++--- 4 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 scm-plugins/scm-legacy-plugin/package.json create mode 100644 scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js create mode 100644 scm-plugins/scm-legacy-plugin/src/main/js/index.js diff --git a/scm-plugins/scm-legacy-plugin/package.json b/scm-plugins/scm-legacy-plugin/package.json new file mode 100644 index 0000000000..84f78f419a --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/package.json @@ -0,0 +1,20 @@ +{ + "name": "@scm-manager/legacy-plugin", + "license": "BSD-3-Clause", + "main": "src/main/js/index.js", + "scripts": { + "build": "ui-bundler plugin", + "watch": "ui-bundler plugin -w", + "lint": "ui-bundler lint", + "flow": "flow check" + }, + "dependencies": { + "@scm-manager/ui-components": "latest", + "@scm-manager/ui-extensions": "^0.1.1", + "react-redux": "^5.0.7", + "@scm-manager/ui-types": "latest" + }, + "devDependencies": { + "@scm-manager/ui-bundler": "^0.0.25" + } +} diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js new file mode 100644 index 0000000000..fd87f8e330 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js @@ -0,0 +1,15 @@ +//@flow +import React from "react"; +import { withRouter } from "react-router-dom"; + +class DummyComponent extends React.Component { + + render() { + return ( + <> + + ); + } +} + +export default withRouter(DummyComponent); diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/index.js b/scm-plugins/scm-legacy-plugin/src/main/js/index.js new file mode 100644 index 0000000000..4ddb7a0b04 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/js/index.js @@ -0,0 +1,55 @@ +// @flow + +import React from "react"; +import { withRouter } from "react-router-dom"; +import { binder } from "@scm-manager/ui-extensions"; +import { ProtectedRoute, apiClient } from "@scm-manager/ui-components"; +import DummyComponent from "./DummyComponent"; + +type Props = { + authenticated?: boolean, + + //context objects + history: History +}; + +class LegacyRepositoryRedirect extends React.Component { + constructor(props: Props) { + super(props); + } + + redirectLegacyRepository() { + const { history } = this.props; + if (location.href && location.href.includes("#diffPanel;")) { + let splittedUrl = location.href.split(";"); + let repoId = splittedUrl[1]; + let changeSetId = splittedUrl[2]; + + apiClient.get("/legacy/repository/" + repoId) + .then(response => response.json()) + .then(payload => history.push("/repo/" + payload.namespace + "/" + payload.name + "/changesets/" + changeSetId) + ); + } + } + + render() { + const { authenticated } = this.props; + + return ( + <> + { + authenticated? + this.redirectLegacyRepository(): + + } + + ); + } +} + +binder.bind("legacyRepository.redirect", withRouter(LegacyRepositoryRedirect)); + diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 129b11f5ba..59cd248e07 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -38,15 +38,6 @@ class Main extends React.Component { const { authenticated, links } = this.props; const redirectUrlFactory = binder.getExtension("main.redirect", this.props); let url = "/repos"; - if (location.href && location.href.includes("#diffPanel;")) { - let repoId = location.href.substring(location.href.search("#diffPanel;") + 11, location.href.search("#diffPanel;") + 21); - console.log("RepoId:"); - console.log(repoId); - apiClient.get("/legacy/repository/" + repoId).then(response => - console.log(JSON.parse(response)) - // this.props.history.push("/repo/" + response.responseBody.namespace + "/" + response.responseBody.name) - ); - } if (redirectUrlFactory) { url = redirectUrlFactory(this.props); } @@ -134,7 +125,11 @@ class Main extends React.Component { component={Profile} authenticated={authenticated} /> - + Date: Thu, 4 Jul 2019 14:17:45 +0200 Subject: [PATCH 031/106] Log class name of update step --- scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java index f9d1d5b419..980921d283 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java +++ b/scm-webapp/src/main/java/sonia/scm/update/UpdateEngine.java @@ -55,9 +55,10 @@ public class UpdateEngine { private void execute(UpdateStep updateStep) { try { - LOG.info("running update step for type {} and version {}", + LOG.info("running update step for type {} and version {} (class {})", updateStep.getAffectedDataType(), - updateStep.getTargetVersion() + updateStep.getTargetVersion(), + updateStep.getClass().getName() ); updateStep.doUpdate(); } catch (Exception e) { From 264b38a2df454dbc85ed09d5d0167011df540cd0 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 4 Jul 2019 12:24:45 +0000 Subject: [PATCH 032/106] Close branch feature/migrate_lfs From 99edbce7fd863430dbd799436c80823cdc551de5 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 4 Jul 2019 12:44:40 +0000 Subject: [PATCH 033/106] Close branch bugfix/double_check_directory_creation From 2f58428d2cdf220af8797d00c80911f7ab21be8e Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 5 Jul 2019 06:28:10 +0000 Subject: [PATCH 034/106] Close branch bugfix/repoid_svn_migration From b14c3444bd9979e6368213deb9cc99e5064010af Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 5 Jul 2019 10:08:27 +0200 Subject: [PATCH 035/106] add ErrorHandling and show ErrorNotification --- .../scm/legacy/LegacyRepositoryService.java | 5 +- .../scm-legacy-plugin/src/main/js/index.js | 74 ++++++++++++++----- scm-ui/src/containers/Main.js | 4 +- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java index 9e27c736ea..8cac590135 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java @@ -3,6 +3,7 @@ package sonia.scm.legacy; import com.google.inject.Inject; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import sonia.scm.NotFoundException; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; @@ -33,8 +34,10 @@ public class LegacyRepositoryService { }) public NamespaceAndNameDto getNameAndNamespaceForRepositoryId(@PathParam("id") String repositoryId) { Repository repo = repositoryManager.get(repositoryId); + if (repo == null) { + throw new NotFoundException(Repository.class, repositoryId); + } return new NamespaceAndNameDto(repo.getName(), repo.getNamespace()); - } } diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/index.js b/scm-plugins/scm-legacy-plugin/src/main/js/index.js index 4ddb7a0b04..22e20eced7 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/js/index.js +++ b/scm-plugins/scm-legacy-plugin/src/main/js/index.js @@ -3,7 +3,12 @@ import React from "react"; import { withRouter } from "react-router-dom"; import { binder } from "@scm-manager/ui-extensions"; -import { ProtectedRoute, apiClient } from "@scm-manager/ui-components"; +import { + ProtectedRoute, + apiClient, + ErrorNotification, + ErrorBoundary +} from "@scm-manager/ui-components"; import DummyComponent from "./DummyComponent"; type Props = { @@ -13,11 +18,22 @@ type Props = { history: History }; -class LegacyRepositoryRedirect extends React.Component { - constructor(props: Props) { - super(props); +type State = { + error?: Error +}; + +class LegacyRepositoryRedirect extends React.Component { + constructor(props: Props, state: State) { + super(props, state); + this.state = { error: null }; } + handleError = (error: Error) => { + this.setState({ + error + }); + }; + redirectLegacyRepository() { const { history } = this.props; if (location.href && location.href.includes("#diffPanel;")) { @@ -25,31 +41,53 @@ class LegacyRepositoryRedirect extends React.Component { let repoId = splittedUrl[1]; let changeSetId = splittedUrl[2]; - apiClient.get("/legacy/repository/" + repoId) + apiClient + .get("/legacy/repository/" + repoId) .then(response => response.json()) - .then(payload => history.push("/repo/" + payload.namespace + "/" + payload.name + "/changesets/" + changeSetId) - ); + .then(payload => + history.push( + "/repo/" + + payload.namespace + + "/" + + payload.name + + "/changesets/" + + changeSetId + ) + ) + .catch(this.handleError); } } render() { const { authenticated } = this.props; + const { error } = this.state; + + if (error) { + return ( +
    +
    + + + +
    +
    + ); + } return ( <> - { - authenticated? - this.redirectLegacyRepository(): - - } + {authenticated ? ( + this.redirectLegacyRepository() + ) : ( + + )} ); } } -binder.bind("legacyRepository.redirect", withRouter(LegacyRepositoryRedirect)); - +binder.bind("legacy.redirectRepository", withRouter(LegacyRepositoryRedirect)); diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 59cd248e07..a56f6b767c 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -9,7 +9,7 @@ import Users from "../users/containers/Users"; import Login from "../containers/Login"; import Logout from "../containers/Logout"; -import { ProtectedRoute, apiClient } from "@scm-manager/ui-components"; +import { ProtectedRoute } from "@scm-manager/ui-components"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import CreateUser from "../users/containers/CreateUser"; @@ -126,7 +126,7 @@ class Main extends React.Component { authenticated={authenticated} /> From 22d323367f918616754cc31dcf82072e5d4b83a1 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 5 Jul 2019 10:57:21 +0200 Subject: [PATCH 036/106] add Tests for LegacyRepositoryService --- .../legacy/LegacyRepositoryServiceTest.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java diff --git a/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java new file mode 100644 index 0000000000..169c80eae2 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/test/java/sonia/scm/legacy/LegacyRepositoryServiceTest.java @@ -0,0 +1,43 @@ +package sonia.scm.legacy; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.NotFoundException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class LegacyRepositoryServiceTest { + + @Mock + private RepositoryManager repositoryManager; + + private LegacyRepositoryService legacyRepositoryService; + private final Repository repository = new Repository("abc123", "git", "space", "repo"); + + @Before + public void init() { + legacyRepositoryService = new LegacyRepositoryService(repositoryManager); + } + + @Test + public void findRepositoryNameSpaceAndNameForRepositoryId() { + when(repositoryManager.get(any(String.class))).thenReturn(repository); + NamespaceAndNameDto namespaceAndName = legacyRepositoryService.getNameAndNamespaceForRepositoryId("abc123"); + assertThat(namespaceAndName.getName()).isEqualToIgnoringCase("repo"); + assertThat(namespaceAndName.getNamespace()).isEqualToIgnoringCase("space"); + } + + @Test(expected = NotFoundException.class) + public void shouldGetNotFoundExceptionIfRepositoryNotExists() throws NotFoundException { + when(repositoryManager.get(any(String.class))).thenReturn(null); + legacyRepositoryService.getNameAndNamespaceForRepositoryId("456def"); + } +} From 35683543a7998a21ab2780926d6e8045f9b2b9e0 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 5 Jul 2019 11:35:40 +0200 Subject: [PATCH 037/106] cleanup --- .../main/java/sonia/scm/legacy/LegacyRepositoryService.java | 1 - scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js | 1 - scm-ui/src/containers/Main.js | 5 +---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java index 8cac590135..d6b923a927 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyRepositoryService.java @@ -39,6 +39,5 @@ public class LegacyRepositoryService { } return new NamespaceAndNameDto(repo.getName(), repo.getNamespace()); } - } diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js index fd87f8e330..396558f852 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js +++ b/scm-plugins/scm-legacy-plugin/src/main/js/DummyComponent.js @@ -3,7 +3,6 @@ import React from "react"; import { withRouter } from "react-router-dom"; class DummyComponent extends React.Component { - render() { return ( <> diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index a56f6b767c..deec951b41 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -27,10 +27,7 @@ import Profile from "./Profile"; type Props = { authenticated?: boolean, - links: Links, - - //context objects - history: History + links: Links }; class Main extends React.Component { From 313238d9db8c3407bc5eaeec0cbb3ca7a079932f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 8 Jul 2019 09:03:26 +0000 Subject: [PATCH 038/106] Close branch bugfix/check_permissions_for_permissions From 6bd97bfa09707a6b8a9ce3d5d6ee479bb53df7dd Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 8 Jul 2019 09:38:17 +0000 Subject: [PATCH 039/106] Close branch feature/redirect_migrated_repositories From 4e5d2d330238f34960afe7bee8e263fddc83e72d Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 8 Jul 2019 12:59:37 +0200 Subject: [PATCH 040/106] fix redirect url / use hal links in legacy plugin --- scm-plugins/scm-legacy-plugin/src/main/js/index.js | 10 ++++++---- scm-ui/src/containers/Main.js | 5 ----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/scm-plugins/scm-legacy-plugin/src/main/js/index.js b/scm-plugins/scm-legacy-plugin/src/main/js/index.js index 22e20eced7..97c3eb7e32 100644 --- a/scm-plugins/scm-legacy-plugin/src/main/js/index.js +++ b/scm-plugins/scm-legacy-plugin/src/main/js/index.js @@ -10,9 +10,11 @@ import { ErrorBoundary } from "@scm-manager/ui-components"; import DummyComponent from "./DummyComponent"; +import type {Links} from "@scm-manager/ui-types"; type Props = { authenticated?: boolean, + links: Links, //context objects history: History @@ -35,14 +37,14 @@ class LegacyRepositoryRedirect extends React.Component { }; redirectLegacyRepository() { - const { history } = this.props; + const { history, links } = this.props; if (location.href && location.href.includes("#diffPanel;")) { let splittedUrl = location.href.split(";"); let repoId = splittedUrl[1]; let changeSetId = splittedUrl[2]; apiClient - .get("/legacy/repository/" + repoId) + .get(links.nameAndNamespace.href.replace("{id}", repoId)) .then(response => response.json()) .then(payload => history.push( @@ -50,7 +52,7 @@ class LegacyRepositoryRedirect extends React.Component { payload.namespace + "/" + payload.name + - "/changesets/" + + "/changeset/" + changeSetId ) ) @@ -90,4 +92,4 @@ class LegacyRepositoryRedirect extends React.Component { } } -binder.bind("legacy.redirectRepository", withRouter(LegacyRepositoryRedirect)); +binder.bind("main.route", withRouter(LegacyRepositoryRedirect)); diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index deec951b41..1d358ecc87 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -122,11 +122,6 @@ class Main extends React.Component { component={Profile} authenticated={authenticated} /> - Date: Mon, 8 Jul 2019 14:17:24 +0000 Subject: [PATCH 041/106] Close branch feature/redirect_issuetracker_links From 67a7bcec2dd79522589434d468fc3d9f63f0499a Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 9 Jul 2019 15:37:06 +0200 Subject: [PATCH 042/106] encoded uri revision --- scm-ui/src/repos/sources/containers/Sources.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index ddc469760c..549c970aaf 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -99,7 +99,7 @@ class Sources extends React.Component { return (
    {this.renderBranchSelector()} - + Date: Fri, 12 Jul 2019 08:40:12 +0200 Subject: [PATCH 043/106] fix Test --- .../test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java | 1 + 1 file changed, 1 insertion(+) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 6950d882f4..83a0f073dd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -46,6 +46,7 @@ public class ResourceLinksMock { when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo)); when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo)); when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); + when(resourceLinks.pluginCollection()).thenReturn(new ResourceLinks.PluginCollectionLinks(uriInfo)); return resourceLinks; } From f253571d3aecd9609742271bfddf793dcd13665f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 12 Jul 2019 13:48:48 +0200 Subject: [PATCH 044/106] Translate --- scm-ui/public/locales/de/admin.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index 00793b6dce..dea74053e9 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -44,13 +44,13 @@ "submit": "Speichern" }, "delete": { - "button": "Delete", - "subtitle": "Delete Permission Role", + "button": "Löschen", + "subtitle": "Berechtigungsrolle löschen", "confirmAlert": { - "title": "Delete Permission Role", - "message": "Do you really want to delete this permission role? All users who own this role will lose their permissions.", - "submit": "Yes", - "cancel": "No" + "title": "Berechtigungsrolle löschen?", + "message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.", + "submit": "Ja", + "cancel": "Nein" } } } From 8d6dcd6b2b59708e2cc2583666f703b509d0aa15 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 15 Jul 2019 16:23:15 +0200 Subject: [PATCH 045/106] fix permissions for dry-run --- .../scm/repository/api/RepositoryService.java | 1 - .../sonia/scm/api/v2/resources/MergeResource.java | 15 +++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index e11afa4be9..90978d75ea 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -379,7 +379,6 @@ public final class RepositoryService implements Closeable { * @since 2.0.0 */ public MergeCommandBuilder getMergeCommand() { - RepositoryPermissions.push(getRepository()).check(); LOG.debug("create merge command for repository {}", repository.getNamespaceAndName()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java index 2214b258c6..df59ba2abc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MergeResource.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpStatus; import sonia.scm.ConcurrentModificationException; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.MergeCommandBuilder; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; @@ -49,6 +50,7 @@ public class MergeResource { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision()); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { + RepositoryPermissions.push(repositoryService.getRepository()).check(); MergeCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).executeMerge(); if (mergeCommandResult.isSuccess()) { return Response.noContent().build(); @@ -67,14 +69,19 @@ public class MergeResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response dryRun(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) { + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision()); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { - MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun(); - if (mergeCommandResult.isMergeable()) { - return Response.noContent().build(); + if (RepositoryPermissions.push(repositoryService.getRepository()).isPermitted()) { + MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun(); + if (mergeCommandResult.isMergeable()) { + return Response.noContent().build(); + } else { + throw new ConcurrentModificationException("revision", mergeCommand.getTargetRevision()); + } } else { - throw new ConcurrentModificationException("revision", mergeCommand.getTargetRevision()); + return Response.noContent().build(); } } } From 00e051a45f2c8a50d1bf14cff573c0170f75874f Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 16 Jul 2019 08:07:44 +0200 Subject: [PATCH 046/106] fix Tests / add Test --- .../api/v2/resources/MergeResourceTest.java | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java index d47ff35e5f..b9e720fc71 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MergeResourceTest.java @@ -1,10 +1,14 @@ package sonia.scm.api.v2.resources; +import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; import com.google.inject.util.Providers; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -13,6 +17,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; import sonia.scm.repository.api.MergeCommandBuilder; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; @@ -26,12 +31,20 @@ import java.net.URL; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; +import static sonia.scm.repository.RepositoryTestData.createHeartOfGold; @ExtendWith(MockitoExtension.class) +@SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini", + username = "trillian", + password = "secret" +) public class MergeResourceTest extends RepositoryTestBase { public static final String MERGE_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/merge/"; + private Repository repository = createHeartOfGold(); private Dispatcher dispatcher; @Mock @@ -72,10 +85,21 @@ public class MergeResourceTest extends RepositoryTestBase { @Nested class ExecutingMergeCommand { + @Mock + private Subject subject; + @BeforeEach void initRepository() { when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); - when(repositoryService.getMergeCommand()).thenReturn(mergeCommandBuilder); + lenient().when(repositoryService.getMergeCommand()).thenReturn(mergeCommandBuilder); + when(repositoryService.getRepository()).thenReturn(repository); + + ThreadContext.bind(subject); + } + + @AfterEach + void tearDownShiro() { + ThreadContext.unbindSubject(); } @Test @@ -115,6 +139,7 @@ public class MergeResourceTest extends RepositoryTestBase { @Test void shouldHandleSuccessfulDryRun() throws Exception { + when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(true); when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(true)); URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json"); @@ -132,6 +157,7 @@ public class MergeResourceTest extends RepositoryTestBase { @Test void shouldHandleFailedDryRun() throws Exception { + when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(true); when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(false)); URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json"); @@ -146,5 +172,22 @@ public class MergeResourceTest extends RepositoryTestBase { assertThat(response.getStatus()).isEqualTo(409); } + + @Test + void shouldSkipDryRunIfSubjectHasNoPushPermission() throws Exception { + when(subject.isPermitted("repository:push:" + repositoryService.getRepository().getId())).thenReturn(false); + + URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json"); + byte[] mergeCommandJson = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .post(MERGE_URL + "dry-run/") + .content(mergeCommandJson) + .contentType(VndMediaType.MERGE_COMMAND); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(204); + } } } From 4afed7cb74731a1663aa714575cc3f297602ce22 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 16 Jul 2019 16:26:34 +0200 Subject: [PATCH 047/106] migrate securityV1 permissions --- .../scm/security/AssignedPermission.java | 2 +- .../security/XmlSecurityV1UpdateStep.java | 67 ++++++++++++++++++- .../scm/update/user/XmlUserV1UpdateStep.java | 19 +++--- 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java index c98d81f8ba..cc7ff87534 100644 --- a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java @@ -162,7 +162,7 @@ public class AssignedPermission implements PermissionObject, Serializable //J- return MoreObjects.toStringHelper(this) .add("name", name) - .add("groupPermisison", groupPermission) + .add("groupPermission", groupPermission) .add("permission", permission) .toString(); //J+ diff --git a/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java index f62b81b5df..fba3e6a929 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java @@ -21,6 +21,7 @@ import javax.xml.bind.annotation.XmlRootElement; import java.io.File; import java.nio.file.Path; import java.util.Arrays; +import java.util.List; import java.util.function.Consumer; import static java.util.Optional.ofNullable; @@ -46,6 +47,44 @@ public class XmlSecurityV1UpdateStep implements UpdateStep { forAllAdmins(user -> createSecurityEntry(user, false, securityStore), group -> createSecurityEntry(group, true, securityStore)); + + mapV1Permissions(securityStore); + } + + private void mapV1Permissions(ConfigurationEntryStore securityStore) throws JAXBException { + Path v1SecurityFile = determineConfigDirectory().resolve("securityV1" + StoreConstants.FILE_EXTENSION); + + if (!v1SecurityFile.toFile().exists()) { + LOG.info("no v1 file for security found"); + return; + } + + JAXBContext jaxbContext = JAXBContext.newInstance(XmlSecurityV1UpdateStep.V1Security.class); + V1Security v1Security = (V1Security) jaxbContext.createUnmarshaller().unmarshal(v1SecurityFile.toFile()); + + v1Security.entries.forEach(assignedPermission -> { + + String newPermission = ""; + if (assignedPermission.value.permission != null && !assignedPermission.value.permission.isEmpty()) { + String[] splitPermission = assignedPermission.value.permission.split(":"); + switch(splitPermission[2]) { + case "OWNER": + newPermission = "repository:*"; + break; + case "WRITE": + newPermission = "repository:read,pull,push:*"; + break; + case "READ": + newPermission = "repository:read,pull:*"; + } + } + + securityStore.put(new AssignedPermission( + assignedPermission.value.name, + Boolean.parseBoolean(assignedPermission.value.groupPermission), + newPermission + )); + }); } private void forAllAdmins(Consumer userConsumer, Consumer groupConsumer) throws JAXBException { @@ -70,10 +109,9 @@ public class XmlSecurityV1UpdateStep implements UpdateStep { Arrays.stream(entries.split(",")).forEach(consumer); } - @Override public Version getTargetVersion() { - return parse("2.0.0"); + return parse("2.0.1"); } @Override @@ -102,4 +140,29 @@ public class XmlSecurityV1UpdateStep implements UpdateStep { @XmlElement(name = "admin-groups") private String adminGroups; } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "configuration") + private static class V1Security { + @XmlElement(name = "entry") + private List entries; + } + + @XmlAccessorType(XmlAccessType.FIELD) + private static class Entry { + @XmlElement(name = "key") + private String key; + @XmlElement(name = "value") + private Value value; + } + + @XmlAccessorType(XmlAccessType.FIELD) + private static class Value { + @XmlElement(name = "permission") + String permission; + @XmlElement(name = "name") + String name; + @XmlElement(name = "group-permission") + String groupPermission; + } } diff --git a/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java index b2da69fd9b..f2561ac53e 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/user/XmlUserV1UpdateStep.java @@ -57,7 +57,8 @@ public class XmlUserV1UpdateStep implements UpdateStep { @Override public void doUpdate() throws JAXBException { - Optional v1UsersFile = determineV1File(); + Optional v1UsersFile = determineV1File("users"); + determineV1File("security"); if (!v1UsersFile.isPresent()) { LOG.info("no v1 file for users found"); return; @@ -107,17 +108,17 @@ public class XmlUserV1UpdateStep implements UpdateStep { return configurationEntryStoreFactory.withType(AssignedPermission.class).withName("security").build(); } - private Optional determineV1File() { - Path existingUsersFile = resolveConfigFile("users"); - Path usersV1File = resolveConfigFile("usersV1"); - if (existingUsersFile.toFile().exists()) { + private Optional determineV1File(String filename) { + Path existingFile = resolveConfigFile(filename); + Path v1File = resolveConfigFile(filename + "V1"); + if (existingFile.toFile().exists()) { try { - Files.move(existingUsersFile, usersV1File); + Files.move(existingFile, v1File); } catch (IOException e) { - throw new UpdateException("could not move old users file to " + usersV1File.toAbsolutePath()); + throw new UpdateException("could not move old " + filename + " file to " + v1File.toAbsolutePath()); } - LOG.info("moved old users file to {}", usersV1File.toAbsolutePath()); - return of(usersV1File); + LOG.info("moved old " + filename + " file to {}", v1File.toAbsolutePath()); + return of(v1File); } return empty(); } From 0840109d56ea4f4b00f42a4d8302e0ea3c713225 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 16 Jul 2019 16:27:50 +0200 Subject: [PATCH 048/106] add securityV1Migration-Test --- .../security/XmlSecurityV1UpdateStepTest.java | 20 +++++++++++++++++++ .../sonia/scm/update/security/securityV1.xml | 19 ++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml diff --git a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java index d0115e3426..76914d3bfa 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java @@ -56,6 +56,13 @@ class XmlSecurityV1UpdateStepTest { copyTestDatabaseFile(configDir, "config.xml"); } + @BeforeEach + void createSecurityV1XML(@TempDirectory.TempDir Path tempDir) throws IOException { + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + copyTestDatabaseFile(configDir, "securityV1.xml"); + } + @Test void shouldCreatePermissionForUsersConfiguredAsAdmin() throws JAXBException { updateStep.doUpdate(); @@ -81,6 +88,19 @@ class XmlSecurityV1UpdateStepTest { .collect(toList()); assertThat(assignedPermission).contains("admins", "vogons"); } + + @Test + void shouldMapV1PermissionsFromSecurityV1XML() throws JAXBException { + updateStep.doUpdate(); + List assignedPermission = + assignedPermissionStore.getAll().values() + .stream() + .filter(a -> a.getPermission().getValue().contains("repository:")) + .map(AssignedPermission::getName) + .collect(toList()); + assertThat(assignedPermission).contains("scmadmin"); + assertThat(assignedPermission).contains("test"); + } } private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException { diff --git a/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml b/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml new file mode 100644 index 0000000000..2f23062f42 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml @@ -0,0 +1,19 @@ + + + + 4lRWOA7DH1 + + false + scmadmin + repository:*:READ + + + + CfRWOAANM2 + + true + test + repository:*:OWNER + + + From 3e34e9183be6411c9d2007327c2a906ff196ef88 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 17 Jul 2019 10:12:39 +0200 Subject: [PATCH 049/106] made changeset tags round and colored them again --- .../packages/ui-components/src/repos/DiffFile.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js index 3d2dba6c8b..a921a4a0a4 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js @@ -178,12 +178,20 @@ class DiffFile extends React.Component { if (key === value) { value = file.type; } + const color = + value === "added" + ? "is-success" + : value === "deleted" + ? "is-danger" + : "is-info"; + return ( From ee6d3c4be34073f837f068ecaae1d0fee9d98c28 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 17 Jul 2019 11:11:40 +0000 Subject: [PATCH 050/106] Close branch feature/explicit_tasks From 5774d8a1ee5abe0b3597f7fceafaaef95fbbfdc6 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 17 Jul 2019 17:35:29 +0200 Subject: [PATCH 051/106] added inherit styling and pass className --- .../packages/ui-components/src/Notification.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/Notification.js b/scm-ui-components/packages/ui-components/src/Notification.js index b48ae16851..46b6b8edcf 100644 --- a/scm-ui-components/packages/ui-components/src/Notification.js +++ b/scm-ui-components/packages/ui-components/src/Notification.js @@ -2,11 +2,18 @@ import * as React from "react"; import classNames from "classnames"; -type NotificationType = "primary" | "info" | "success" | "warning" | "danger"; +type NotificationType = + | "primary" + | "info" + | "success" + | "warning" + | "danger" + | "inherit"; type Props = { type: NotificationType, onClose?: () => void, + className?: string, children?: React.Node }; @@ -24,9 +31,12 @@ class Notification extends React.Component { } render() { - const { type, children } = this.props; + const { type, className, children } = this.props; + + const color = type !== "inherit" ? "is-" + type : ""; + return ( -
    +
    {this.renderCloseButton()} {children}
    From 272f249eeae706904e6202f20653ff75e96037b4 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 18 Jul 2019 09:00:59 +0000 Subject: [PATCH 052/106] Close branch bugfix/permissionReadCannotOpenPR From a4cb2f7caa6ea31192dbf3844b626d6a072678e9 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 18 Jul 2019 12:17:20 +0200 Subject: [PATCH 053/106] fix migration with unknown permissions --- .../security/XmlSecurityV1UpdateStep.java | 50 +++++++++++-------- .../security/XmlSecurityV1UpdateStepTest.java | 20 +++++--- .../sonia/scm/update/security/securityV1.xml | 48 ++++++++++++------ 3 files changed, 75 insertions(+), 43 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java index fba3e6a929..8dac2b5073 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java +++ b/scm-webapp/src/main/java/sonia/scm/update/security/XmlSecurityV1UpdateStep.java @@ -23,6 +23,8 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static java.util.Optional.ofNullable; import static sonia.scm.version.Version.parse; @@ -30,6 +32,8 @@ import static sonia.scm.version.Version.parse; @Extension public class XmlSecurityV1UpdateStep implements UpdateStep { + private static final Pattern v1PermissionPattern = Pattern.compile("^repository:\\*:(READ|WRITE|OWNER)$"); + private static final Logger LOG = LoggerFactory.getLogger(XmlSecurityV1UpdateStep.class); private final SCMContextProvider contextProvider; @@ -63,30 +67,36 @@ public class XmlSecurityV1UpdateStep implements UpdateStep { V1Security v1Security = (V1Security) jaxbContext.createUnmarshaller().unmarshal(v1SecurityFile.toFile()); v1Security.entries.forEach(assignedPermission -> { - - String newPermission = ""; - if (assignedPermission.value.permission != null && !assignedPermission.value.permission.isEmpty()) { - String[] splitPermission = assignedPermission.value.permission.split(":"); - switch(splitPermission[2]) { - case "OWNER": - newPermission = "repository:*"; - break; - case "WRITE": - newPermission = "repository:read,pull,push:*"; - break; - case "READ": - newPermission = "repository:read,pull:*"; - } + Matcher matcher = v1PermissionPattern.matcher(assignedPermission.value.permission); + if (matcher.matches()) { + String newPermission = convertRole(matcher.group(1)); + securityStore.put(new AssignedPermission( + assignedPermission.value.name, + Boolean.parseBoolean(assignedPermission.value.groupPermission), + newPermission + )); } - - securityStore.put(new AssignedPermission( - assignedPermission.value.name, - Boolean.parseBoolean(assignedPermission.value.groupPermission), - newPermission - )); }); } + private String convertRole(String role) { + String newPermission; + switch (role) { + case "OWNER": + newPermission = "repository:*"; + break; + case "WRITE": + newPermission = "repository:read,pull,push:*"; + break; + case "READ": + newPermission = "repository:read,pull:*"; + break; + default: + newPermission = ""; + } + return newPermission; + } + private void forAllAdmins(Consumer userConsumer, Consumer groupConsumer) throws JAXBException { Path configDirectory = determineConfigDirectory(); Path existingConfigFile = configDirectory.resolve("config" + StoreConstants.FILE_EXTENSION); diff --git a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java index 76914d3bfa..73c7fe6aca 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/security/XmlSecurityV1UpdateStepTest.java @@ -56,13 +56,6 @@ class XmlSecurityV1UpdateStepTest { copyTestDatabaseFile(configDir, "config.xml"); } - @BeforeEach - void createSecurityV1XML(@TempDirectory.TempDir Path tempDir) throws IOException { - Path configDir = tempDir.resolve("config"); - Files.createDirectories(configDir); - copyTestDatabaseFile(configDir, "securityV1.xml"); - } - @Test void shouldCreatePermissionForUsersConfiguredAsAdmin() throws JAXBException { updateStep.doUpdate(); @@ -89,6 +82,18 @@ class XmlSecurityV1UpdateStepTest { assertThat(assignedPermission).contains("admins", "vogons"); } + } + + @Nested + class WithExistingSecurityXml { + + @BeforeEach + void createSecurityV1XML(@TempDirectory.TempDir Path tempDir) throws IOException { + Path configDir = tempDir.resolve("config"); + Files.createDirectories(configDir); + copyTestDatabaseFile(configDir, "securityV1.xml"); + } + @Test void shouldMapV1PermissionsFromSecurityV1XML() throws JAXBException { updateStep.doUpdate(); @@ -101,6 +106,7 @@ class XmlSecurityV1UpdateStepTest { assertThat(assignedPermission).contains("scmadmin"); assertThat(assignedPermission).contains("test"); } + } private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException { diff --git a/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml b/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml index 2f23062f42..8de82f88d9 100644 --- a/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml +++ b/scm-webapp/src/test/resources/sonia/scm/update/security/securityV1.xml @@ -1,19 +1,35 @@ - - 4lRWOA7DH1 - - false - scmadmin - repository:*:READ - - - - CfRWOAANM2 - - true - test - repository:*:OWNER - - + + 4lRWOA7DH1 + + false + scmadmin + repository:*:READ + + + + CfRWOAANM2 + + true + test + repository:*:OWNER + + + + CfRWOAANM2 + + true + test + invalid:permission + + + + CfRWOAANM2 + + true + test + repository:*:STRANGE + + From f9c023330d2214cdc1c38b2100f0d1b2bf147a22 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 19 Jul 2019 05:15:27 +0000 Subject: [PATCH 054/106] Close branch bugfix/migrate_security_xml From 4a275c445e3e75963c44644d780c069f645f2505 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 22 Jul 2019 09:44:03 +0200 Subject: [PATCH 055/106] removed unnecessary request provider --- .../sonia/scm/web/protocol/HttpProtocolServlet.java | 10 ++-------- .../scm/web/protocol/HttpProtocolServletTest.java | 13 ++----------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java index 250846a8cc..7aae2f036c 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -16,7 +16,6 @@ import sonia.scm.repository.spi.HttpScmProtocol; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; -import javax.inject.Provider; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -33,17 +32,13 @@ public class HttpProtocolServlet extends HttpServlet { public static final String PATTERN = PATH + "/*"; private final RepositoryServiceFactory serviceFactory; - - private final Provider requestProvider; - private final PushStateDispatcher dispatcher; private final UserAgentParser userAgentParser; @Inject - public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, Provider requestProvider, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { + public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { this.serviceFactory = serviceFactory; - this.requestProvider = requestProvider; this.dispatcher = dispatcher; this.userAgentParser = userAgentParser; } @@ -55,7 +50,6 @@ public class HttpProtocolServlet extends HttpServlet { log.trace("dispatch browser request for user agent {}", userAgent); dispatcher.dispatch(request, response, request.getRequestURI()); } else { - String pathInfo = request.getPathInfo(); Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(pathInfo); if (namespaceAndName.isPresent()) { @@ -69,7 +63,7 @@ public class HttpProtocolServlet extends HttpServlet { private void service(HttpServletRequest req, HttpServletResponse resp, NamespaceAndName namespaceAndName) throws IOException, ServletException { try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { - requestProvider.get().setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository()); + req.setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository()); HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); protocol.serve(req, resp, getServletConfig()); } catch (NotFoundException e) { diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java index 1bd6358c95..82088c6499 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java @@ -15,7 +15,6 @@ import sonia.scm.repository.spi.HttpScmProtocol; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; -import javax.inject.Provider; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -23,24 +22,17 @@ import java.io.IOException; import static org.mockito.AdditionalMatchers.not; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; public class HttpProtocolServletTest { - @Mock private RepositoryServiceFactory serviceFactory; @Mock - private HttpServletRequest httpServletRequest; - @Mock private PushStateDispatcher dispatcher; @Mock private UserAgentParser userAgentParser; - @Mock - private Provider requestProvider; @InjectMocks private HttpProtocolServlet servlet; @@ -65,7 +57,6 @@ public class HttpProtocolServletTest { NamespaceAndName existingRepo = new NamespaceAndName("space", "repo"); when(serviceFactory.create(not(eq(existingRepo)))).thenThrow(new NotFoundException("Test", "a")); when(serviceFactory.create(existingRepo)).thenReturn(repositoryService); - when(requestProvider.get()).thenReturn(httpServletRequest); } @Test @@ -107,7 +98,7 @@ public class HttpProtocolServletTest { servlet.service(request, response); - verify(httpServletRequest).setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repository); + verify(request).setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repository); verify(protocol).serve(request, response, null); verify(repositoryService).close(); } From 56a683c7c5b75059b59e53acde165b48c7020b65 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 22 Jul 2019 13:00:49 +0200 Subject: [PATCH 056/106] fix checkout of repositories with dots in the names --- .../scm/web/protocol/HttpProtocolServlet.java | 6 +- .../NamespaceAndNameFromPathExtractor.java | 28 ++++++-- ...NamespaceAndNameFromPathExtractorTest.java | 66 +++++++++++++++++-- 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java index 7aae2f036c..623be728c1 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -32,13 +32,15 @@ public class HttpProtocolServlet extends HttpServlet { public static final String PATTERN = PATH + "/*"; private final RepositoryServiceFactory serviceFactory; + private final NamespaceAndNameFromPathExtractor pathExtractor; private final PushStateDispatcher dispatcher; private final UserAgentParser userAgentParser; @Inject - public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { + public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, NamespaceAndNameFromPathExtractor pathExtractor, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { this.serviceFactory = serviceFactory; + this.pathExtractor = pathExtractor; this.dispatcher = dispatcher; this.userAgentParser = userAgentParser; } @@ -51,7 +53,7 @@ public class HttpProtocolServlet extends HttpServlet { dispatcher.dispatch(request, response, request.getRequestURI()); } else { String pathInfo = request.getPathInfo(); - Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(pathInfo); + Optional namespaceAndName = pathExtractor.fromUri(pathInfo); if (namespaceAndName.isPresent()) { service(request, response, namespaceAndName.get()); } else { diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java index 22e2433561..2c1eeb1b90 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java @@ -1,18 +1,31 @@ package sonia.scm.web.protocol; +import sonia.scm.Type; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryManager; import sonia.scm.util.HttpUtil; +import javax.inject.Inject; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static java.util.Optional.empty; import static java.util.Optional.of; final class NamespaceAndNameFromPathExtractor { - private NamespaceAndNameFromPathExtractor() {} + private final Set types; - static Optional fromUri(String uri) { + @Inject + public NamespaceAndNameFromPathExtractor(RepositoryManager repositoryManager) { + this.types = repositoryManager.getConfiguredTypes() + .stream() + .map(Type::getName) + .collect(Collectors.toSet()); + } + + Optional fromUri(String uri) { if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) { uri = uri.substring(1); } @@ -30,12 +43,13 @@ final class NamespaceAndNameFromPathExtractor { } String name = uri.substring(endOfNamespace + 1, nameIndex); - - int nameDotIndex = name.indexOf('.'); + int nameDotIndex = name.lastIndexOf('.'); if (nameDotIndex >= 0) { - return of(new NamespaceAndName(namespace, name.substring(0, nameDotIndex))); - } else { - return of(new NamespaceAndName(namespace, name)); + String suffix = name.substring(nameDotIndex + 1); + if (types.contains(suffix)) { + name = name.substring(0, nameDotIndex); + } } + return of(new NamespaceAndName(namespace, name)); } } diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java index 0998010069..5ac8f37c01 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java @@ -1,17 +1,48 @@ package sonia.scm.web.protocol; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryType; +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class NamespaceAndNameFromPathExtractorTest { + + @Mock + private RepositoryManager repositoryManager; + + private NamespaceAndNameFromPathExtractor extractor; + + @BeforeEach + void setUpObjectUnderTest() { + List types = Arrays.asList( + new RepositoryType("git", "Git", Collections.emptySet()), + new RepositoryType("hg", "Mercurial", Collections.emptySet()), + new RepositoryType("svn", "Subversion", Collections.emptySet()) + ); + when(repositoryManager.getConfiguredTypes()).thenReturn(types); + extractor = new NamespaceAndNameFromPathExtractor(repositoryManager); + } -public class NamespaceAndNameFromPathExtractorTest { @TestFactory Stream shouldExtractCorrectNamespaceAndName() { return Stream.of( @@ -26,21 +57,26 @@ public class NamespaceAndNameFromPathExtractorTest { } @TestFactory - Stream shouldHandleTrailingDotSomethings() { + Stream shouldHandleTypeSuffix() { return Stream.of( "/space/repo.git", - "/space/repo.and.more", - "/space/repo." + "/space/repo.hg", + "/space/repo.svn", + "/space/repo" ).map(this::createCorrectTest); } private DynamicTest createCorrectTest(String path) { + return createCorrectTest(path, new NamespaceAndName("space", "repo")); + } + + private DynamicTest createCorrectTest(String path, NamespaceAndName expected) { return dynamicTest( "should extract correct namespace and name for path " + path, () -> { - Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(path); + Optional namespaceAndName = extractor.fromUri(path); - assertThat(namespaceAndName.get()).isEqualTo(new NamespaceAndName("space", "repo")); + assertThat(namespaceAndName.get()).isEqualTo(expected); } ); } @@ -59,10 +95,26 @@ public class NamespaceAndNameFromPathExtractorTest { return dynamicTest( "should not fail for wrong path " + path, () -> { - Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(path); + Optional namespaceAndName = extractor.fromUri(path); assertThat(namespaceAndName.isPresent()).isFalse(); } ); } + + @TestFactory + Stream shouldHandleDots() { + return Stream.of( + "/space/repo.with.dots.git", + "/space/repo.with.dots.hg", + "/space/repo.with.dots.svn", + "/space/repo.with.dots" + ).map(path -> createCorrectTest(path, new NamespaceAndName("space", "repo.with.dots"))); + } + + @Test + void shouldNotFailOnEndingDot() { + Optional namespaceAndName = extractor.fromUri("/space/repo."); + assertThat(namespaceAndName).contains(new NamespaceAndName("space", "repo.")); + } } From 405ffcf164c1fce0e3aa933043c147a17e460d7d Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 22 Jul 2019 14:26:15 +0200 Subject: [PATCH 057/106] migrate tests to junit 5 --- .../web/protocol/HttpProtocolServletTest.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java index 82088c6499..3c1009106a 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java @@ -1,9 +1,11 @@ package sonia.scm.web.protocol; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; import sonia.scm.PushStateDispatcher; import sonia.scm.repository.DefaultRepositoryProvider; @@ -23,9 +25,9 @@ import java.io.IOException; import static org.mockito.AdditionalMatchers.not; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.initMocks; -public class HttpProtocolServletTest { +@ExtendWith(MockitoExtension.class) +class HttpProtocolServletTest { @Mock private RepositoryServiceFactory serviceFactory; @@ -49,9 +51,8 @@ public class HttpProtocolServletTest { @Mock private HttpScmProtocol protocol; - @Before - public void init() { - initMocks(this); + @BeforeEach + void init() { when(userAgentParser.parse(request)).thenReturn(userAgent); when(userAgent.isBrowser()).thenReturn(false); NamespaceAndName existingRepo = new NamespaceAndName("space", "repo"); @@ -60,7 +61,7 @@ public class HttpProtocolServletTest { } @Test - public void shouldDispatchBrowserRequests() throws ServletException, IOException { + void shouldDispatchBrowserRequests() throws ServletException, IOException { when(userAgent.isBrowser()).thenReturn(true); when(request.getRequestURI()).thenReturn("uri"); @@ -70,7 +71,7 @@ public class HttpProtocolServletTest { } @Test - public void shouldHandleBadPaths() throws IOException, ServletException { + void shouldHandleBadPaths() throws IOException, ServletException { when(request.getPathInfo()).thenReturn("/illegal"); servlet.service(request, response); @@ -79,7 +80,7 @@ public class HttpProtocolServletTest { } @Test - public void shouldHandleNotExistingRepository() throws IOException, ServletException { + void shouldHandleNotExistingRepository() throws IOException, ServletException { when(request.getPathInfo()).thenReturn("/not/exists"); servlet.service(request, response); @@ -88,7 +89,7 @@ public class HttpProtocolServletTest { } @Test - public void shouldDelegateToProvider() throws IOException, ServletException { + void shouldDelegateToProvider() throws IOException, ServletException { when(request.getPathInfo()).thenReturn("/space/name"); NamespaceAndName namespaceAndName = new NamespaceAndName("space", "name"); doReturn(repositoryService).when(serviceFactory).create(namespaceAndName); From 4ba94374104e2d7593af6f3586c7d2096f72cba8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 22 Jul 2019 14:41:12 +0200 Subject: [PATCH 058/106] fixed broken tests --- .../web/protocol/HttpProtocolServletTest.java | 112 +++++++++++------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java index 3c1009106a..59b29c8eda 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java @@ -1,6 +1,7 @@ package sonia.scm.web.protocol; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -11,6 +12,7 @@ import sonia.scm.PushStateDispatcher; import sonia.scm.repository.DefaultRepositoryProvider; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HttpScmProtocol; @@ -21,18 +23,23 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Optional; -import static org.mockito.AdditionalMatchers.not; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class HttpProtocolServletTest { @Mock private RepositoryServiceFactory serviceFactory; + + @Mock + private NamespaceAndNameFromPathExtractor extractor; + @Mock private PushStateDispatcher dispatcher; + @Mock private UserAgentParser userAgentParser; @@ -41,66 +48,89 @@ class HttpProtocolServletTest { @Mock private RepositoryService repositoryService; + @Mock private UserAgent userAgent; @Mock private HttpServletRequest request; + @Mock private HttpServletResponse response; + @Mock private HttpScmProtocol protocol; - @BeforeEach - void init() { - when(userAgentParser.parse(request)).thenReturn(userAgent); - when(userAgent.isBrowser()).thenReturn(false); - NamespaceAndName existingRepo = new NamespaceAndName("space", "repo"); - when(serviceFactory.create(not(eq(existingRepo)))).thenThrow(new NotFoundException("Test", "a")); - when(serviceFactory.create(existingRepo)).thenReturn(repositoryService); + @Nested + class Browser { + + @BeforeEach + void prepareMocks() { + when(userAgentParser.parse(request)).thenReturn(userAgent); + when(userAgent.isBrowser()).thenReturn(true); + when(request.getRequestURI()).thenReturn("uri"); + } + + @Test + void shouldDispatchBrowserRequests() throws ServletException, IOException { + when(userAgent.isBrowser()).thenReturn(true); + when(request.getRequestURI()).thenReturn("uri"); + + servlet.service(request, response); + + verify(dispatcher).dispatch(request, response, "uri"); + } + } - @Test - void shouldDispatchBrowserRequests() throws ServletException, IOException { - when(userAgent.isBrowser()).thenReturn(true); - when(request.getRequestURI()).thenReturn("uri"); + @Nested + class ScmClient { - servlet.service(request, response); + @BeforeEach + void prepareMocks() { + when(userAgentParser.parse(request)).thenReturn(userAgent); + when(userAgent.isBrowser()).thenReturn(false); + } - verify(dispatcher).dispatch(request, response, "uri"); - } + @Test + void shouldHandleBadPaths() throws IOException, ServletException { + when(request.getPathInfo()).thenReturn("/illegal"); - @Test - void shouldHandleBadPaths() throws IOException, ServletException { - when(request.getPathInfo()).thenReturn("/illegal"); + servlet.service(request, response); - servlet.service(request, response); + verify(response).setStatus(400); + } - verify(response).setStatus(400); - } + @Test + void shouldHandleNotExistingRepository() throws IOException, ServletException { + when(request.getPathInfo()).thenReturn("/not/exists"); - @Test - void shouldHandleNotExistingRepository() throws IOException, ServletException { - when(request.getPathInfo()).thenReturn("/not/exists"); + NamespaceAndName repo = new NamespaceAndName("not", "exists"); + when(extractor.fromUri("/not/exists")).thenReturn(Optional.of(repo)); + when(serviceFactory.create(repo)).thenThrow(new NotFoundException("Test", "a")); - servlet.service(request, response); + servlet.service(request, response); - verify(response).setStatus(404); - } + verify(response).setStatus(404); + } - @Test - void shouldDelegateToProvider() throws IOException, ServletException { - when(request.getPathInfo()).thenReturn("/space/name"); - NamespaceAndName namespaceAndName = new NamespaceAndName("space", "name"); - doReturn(repositoryService).when(serviceFactory).create(namespaceAndName); - Repository repository = new Repository(); - when(repositoryService.getRepository()).thenReturn(repository); - when(repositoryService.getProtocol(HttpScmProtocol.class)).thenReturn(protocol); + @Test + void shouldDelegateToProvider() throws IOException, ServletException { + NamespaceAndName repo = new NamespaceAndName("space", "name"); + when(extractor.fromUri("/space/name")).thenReturn(Optional.of(repo)); + when(serviceFactory.create(repo)).thenReturn(repositoryService); - servlet.service(request, response); + when(request.getPathInfo()).thenReturn("/space/name"); + Repository repository = RepositoryTestData.createHeartOfGold(); + when(repositoryService.getRepository()).thenReturn(repository); + when(repositoryService.getProtocol(HttpScmProtocol.class)).thenReturn(protocol); + + servlet.service(request, response); + + verify(request).setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repository); + verify(protocol).serve(request, response, null); + verify(repositoryService).close(); + } - verify(request).setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repository); - verify(protocol).serve(request, response, null); - verify(repositoryService).close(); } } From cd03d47a3e9dd1a3241654c90f552d69654a6032 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 23 Jul 2019 07:41:48 +0000 Subject: [PATCH 059/106] Close branch bugfix/repository_names_with_dot From 474b9a99a0534a7ecbc8914157e79dfc71cb4c2a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 24 Jul 2019 08:56:12 +0200 Subject: [PATCH 060/106] add bulma popover / add Extensionpoint --- .../src/repos/changesets/ChangesetButtonGroup.js | 2 +- .../src/repos/changesets/ChangesetRow.js | 13 +++++++++++-- .../packages/ui-types/src/Changesets.js | 4 ++-- scm-ui/package.json | 1 + scm-ui/src/repos/containers/RepositoryRoot.js | 2 +- scm-ui/styles/scm.scss | 7 +++++++ scm-ui/yarn.lock | 5 +++++ 7 files changed, 28 insertions(+), 6 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js index 72ce9a2b38..8d46a9f0b4 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js @@ -21,7 +21,7 @@ class ChangesetButtonGroup extends React.Component { const sourcesLink = createSourcesLink(repository, changeset); return ( - +
    - +
    + +
    + +
    +
    diff --git a/scm-ui-components/packages/ui-types/src/Changesets.js b/scm-ui-components/packages/ui-types/src/Changesets.js index cab4233a7f..124fda3831 100644 --- a/scm-ui-components/packages/ui-types/src/Changesets.js +++ b/scm-ui-components/packages/ui-types/src/Changesets.js @@ -1,9 +1,9 @@ //@flow -import type {Links} from "./hal"; +import type {Collection, Links} from "./hal"; import type {Tag} from "./Tags"; import type {Branch} from "./Branches"; -export type Changeset = { +export type Changeset = Collection & { id: string, date: Date, author: { diff --git a/scm-ui/package.json b/scm-ui/package.json index dfb369e61e..a790935001 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -9,6 +9,7 @@ "@fortawesome/fontawesome-free": "^5.3.1", "@scm-manager/ui-extensions": "^0.1.2", "bulma": "^0.7.1", + "bulma-popover": "^1.0.0", "bulma-tooltip": "^2.0.2", "classnames": "^2.2.5", "font-awesome": "^4.7.0", diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 6630086964..5cb2078e5a 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -125,7 +125,7 @@ class RepositoryRoot extends React.Component { return (
    -
    +
    Date: Wed, 24 Jul 2019 13:55:53 +0200 Subject: [PATCH 061/106] Add method to embed collections --- .../java/sonia/scm/api/v2/resources/HalAppender.java | 10 ++++++++++ .../sonia/scm/api/v2/resources/EdisonHalAppender.java | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java index 6afb542646..b313f68af8 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java @@ -2,6 +2,8 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; +import java.util.List; + /** * The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response. * @@ -34,6 +36,14 @@ public interface HalAppender { */ void appendEmbedded(String rel, HalRepresentation embeddedItem); + /** + * Appends a list of embedded objects to the json response. + * + * @param rel name of relation + * @param embeddedItems embedded objects + */ + void appendEmbedded(String rel, List embeddedItems); + /** * Builder for link arrays. */ diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java index 769de2b705..bb89c90556 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java @@ -33,6 +33,11 @@ class EdisonHalAppender implements HalAppender { embeddedBuilder.with(rel, embedded); } + @Override + public void appendEmbedded(String rel, List embedded) { + embeddedBuilder.with(rel, embedded); + } + private static class EdisonLinkArrayBuilder implements LinkArrayBuilder { private final Links.Builder builder; From 31013f8102fd8600a3682e154ea7a3a0d1e08d31 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 24 Jul 2019 16:08:16 +0000 Subject: [PATCH 062/106] Close branch feature/ci_integration From bc37ccef572c2456677271039db2122f18a8991a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 26 Jul 2019 15:07:40 +0200 Subject: [PATCH 063/106] Make protocol auth filter available for legacy paths --- ...otocolServletAuthenticationFilterBase.java | 12 ++-------- ...olServletAuthenticationFilterBaseTest.java | 6 ++--- ...cyProtocolServletAuthenticationFilter.java | 22 +++++++++++++++++++ ...tpProtocolServletAuthenticationFilter.java | 22 +++++++++++++++++++ 4 files changed, 49 insertions(+), 13 deletions(-) rename scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java => scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java (74%) rename scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java => scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java (91%) create mode 100644 scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java create mode 100644 scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java diff --git a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java similarity index 74% rename from scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java rename to scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java index 973b3af4cb..8b1868309b 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java @@ -1,16 +1,11 @@ package sonia.scm.web.filter; -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; import sonia.scm.util.HttpUtil; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; import sonia.scm.web.WebTokenGenerator; -import sonia.scm.web.protocol.HttpProtocolServlet; -import javax.inject.Inject; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -18,14 +13,11 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Set; -@Priority(Filters.PRIORITY_AUTHENTICATION) -@WebElement(value = HttpProtocolServlet.PATTERN) -public class HttpProtocolServletAuthenticationFilter extends AuthenticationFilter { +public class HttpProtocolServletAuthenticationFilterBase extends AuthenticationFilter { private final UserAgentParser userAgentParser; - @Inject - public HttpProtocolServletAuthenticationFilter( + protected HttpProtocolServletAuthenticationFilterBase( ScmConfiguration configuration, Set tokenGenerators, UserAgentParser userAgentParser) { diff --git a/scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java similarity index 91% rename from scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java rename to scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java index ff493e2b84..1f9b4fad07 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterTest.java +++ b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java @@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class HttpProtocolServletAuthenticationFilterTest { +class HttpProtocolServletAuthenticationFilterBaseTest { private ScmConfiguration configuration = new ScmConfiguration(); @@ -32,7 +32,7 @@ class HttpProtocolServletAuthenticationFilterTest { @Mock private UserAgentParser userAgentParser; - private HttpProtocolServletAuthenticationFilter authenticationFilter; + private HttpProtocolServletAuthenticationFilterBase authenticationFilter; @Mock private HttpServletRequest request; @@ -48,7 +48,7 @@ class HttpProtocolServletAuthenticationFilterTest { @BeforeEach void setUpObjectUnderTest() { - authenticationFilter = new HttpProtocolServletAuthenticationFilter(configuration, tokenGenerators, userAgentParser); + authenticationFilter = new HttpProtocolServletAuthenticationFilterBase(configuration, tokenGenerators, userAgentParser); } @Test diff --git a/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java new file mode 100644 index 0000000000..ca6dea0898 --- /dev/null +++ b/scm-plugins/scm-legacy-plugin/src/main/java/sonia/scm/legacy/LegacyProtocolServletAuthenticationFilter.java @@ -0,0 +1,22 @@ +package sonia.scm.legacy; + +import sonia.scm.Priority; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; +import sonia.scm.web.filter.HttpProtocolServletAuthenticationFilterBase; + +import javax.inject.Inject; +import java.util.Set; + +@Priority(Filters.PRIORITY_AUTHENTICATION) +@WebElement(value = "/git/*", morePatterns = {"/hg/*", "/svn/*"}) +public class LegacyProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase { + + @Inject + public LegacyProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set tokenGenerators, UserAgentParser userAgentParser) { + super(configuration, tokenGenerators, userAgentParser); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java b/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java new file mode 100644 index 0000000000..f9b40961a3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/filter/DefaultHttpProtocolServletAuthenticationFilter.java @@ -0,0 +1,22 @@ +package sonia.scm.web.filter; + +import sonia.scm.Priority; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.filter.Filters; +import sonia.scm.filter.WebElement; +import sonia.scm.web.UserAgentParser; +import sonia.scm.web.WebTokenGenerator; +import sonia.scm.web.protocol.HttpProtocolServlet; + +import javax.inject.Inject; +import java.util.Set; + +@Priority(Filters.PRIORITY_AUTHENTICATION) +@WebElement(value = HttpProtocolServlet.PATTERN) +public class DefaultHttpProtocolServletAuthenticationFilter extends HttpProtocolServletAuthenticationFilterBase { + + @Inject + public DefaultHttpProtocolServletAuthenticationFilter(ScmConfiguration configuration, Set tokenGenerators, UserAgentParser userAgentParser) { + super(configuration, tokenGenerators, userAgentParser); + } +} From 64f3647acf820aeb2bdeb054b115a4c6b1179c04 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 26 Jul 2019 16:18:08 +0200 Subject: [PATCH 064/106] Fix unit test --- .../resources/mockito-extensions/org.mockito.plugins.MockMaker | 1 + 1 file changed, 1 insertion(+) create mode 100644 scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/scm-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline From 2fc0b56f63d3b0f7e61209638beb66ebea456a1d Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 29 Jul 2019 08:24:26 +0000 Subject: [PATCH 065/106] Close branch bugfix/legacy_checkout_for_other_realms From e0411ed17f0b29e402b8587a8483689d14c93b84 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 29 Jul 2019 11:22:57 +0200 Subject: [PATCH 066/106] create API for parsed diff result command --- .../api/AbstractDiffCommandBuilder.java | 68 ++++++++++++++++++ .../sonia/scm/repository/api/Command.java | 12 +--- .../repository/api/DiffCommandBuilder.java | 69 +++---------------- .../sonia/scm/repository/api/DiffFile.java | 12 ++++ .../sonia/scm/repository/api/DiffLine.java | 12 ++++ .../sonia/scm/repository/api/DiffResult.java | 8 +++ .../api/DiffResultCommandBuilder.java | 41 +++++++++++ .../java/sonia/scm/repository/api/Hunk.java | 12 ++++ .../scm/repository/api/RepositoryService.java | 15 ++++ .../scm/repository/spi/DiffResultCommand.java | 9 +++ .../spi/RepositoryServiceProvider.java | 5 ++ 11 files changed, 192 insertions(+), 71 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/api/Hunk.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java new file mode 100644 index 0000000000..b5b2f2a08b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/AbstractDiffCommandBuilder.java @@ -0,0 +1,68 @@ +package sonia.scm.repository.api; + +import sonia.scm.FeatureNotSupportedException; +import sonia.scm.repository.Feature; +import sonia.scm.repository.spi.DiffCommandRequest; + +import java.util.Set; + +abstract class AbstractDiffCommandBuilder { + + + /** request for the diff command implementation */ + final DiffCommandRequest request = new DiffCommandRequest(); + + private final Set supportedFeatures; + + AbstractDiffCommandBuilder(Set supportedFeatures) { + this.supportedFeatures = supportedFeatures; + } + + /** + * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given + * here. In other words: What changes would be new to the ancestor changeset given here when the branch would + * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! + * + * @return {@code this} + */ + public T setAncestorChangeset(String revision) + { + if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { + throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name()); + } + request.setAncestorChangeset(revision); + + return self(); + } + + /** + * Show the difference only for the given path. + * + * + * @param path path for difference + * + * @return {@code this} + */ + public T setPath(String path) + { + request.setPath(path); + return self(); + } + + /** + * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this + * and another revision. + * + * + * @param revision revision for difference + * + * @return {@code this} + */ + public T setRevision(String revision) + { + request.setRevision(revision); + return self(); + } + + abstract T self(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index e380727769..3249e54ec3 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -53,11 +53,6 @@ public enum Command */ BRANCHES, - /** - * @since 2.0 - */ - BRANCH, - /** * @since 1.31 */ @@ -71,10 +66,5 @@ public enum Command /** * @since 2.0 */ - MODIFICATIONS, - - /** - * @since 2.0 - */ - MERGE + MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java index 9e7094d5bf..18d4e11a7f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java @@ -38,10 +38,8 @@ package sonia.scm.repository.api; import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.FeatureNotSupportedException; import sonia.scm.repository.Feature; import sonia.scm.repository.spi.DiffCommand; -import sonia.scm.repository.spi.DiffCommandRequest; import sonia.scm.util.IOUtil; import java.io.ByteArrayOutputStream; @@ -72,7 +70,7 @@ import java.util.Set; * @author Sebastian Sdorra * @since 1.17 */ -public final class DiffCommandBuilder +public final class DiffCommandBuilder extends AbstractDiffCommandBuilder { /** @@ -81,6 +79,9 @@ public final class DiffCommandBuilder private static final Logger logger = LoggerFactory.getLogger(DiffCommandBuilder.class); + /** implementation of the diff command */ + private final DiffCommand diffCommand; + //~--- constructors --------------------------------------------------------- /** @@ -92,8 +93,8 @@ public final class DiffCommandBuilder */ DiffCommandBuilder(DiffCommand diffCommand, Set supportedFeatures) { + super(supportedFeatures); this.diffCommand = diffCommand; - this.supportedFeatures = supportedFeatures; } //~--- methods -------------------------------------------------------------- @@ -162,54 +163,6 @@ public final class DiffCommandBuilder return this; } - - /** - * Show the difference only for the given path. - * - * - * @param path path for difference - * - * @return {@code this} - */ - public DiffCommandBuilder setPath(String path) - { - request.setPath(path); - - return this; - } - - /** - * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this - * and another revision. - * - * - * @param revision revision for difference - * - * @return {@code this} - */ - public DiffCommandBuilder setRevision(String revision) - { - request.setRevision(revision); - - return this; - } - /** - * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given - * here. In other words: What changes would be new to the ancestor changeset given here when the branch would - * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! - * - * @return {@code this} - */ - public DiffCommandBuilder setAncestorChangeset(String revision) - { - if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { - throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name()); - } - request.setAncestorChangeset(revision); - - return this; - } - //~--- get methods ---------------------------------------------------------- /** @@ -233,12 +186,8 @@ public final class DiffCommandBuilder diffCommand.getDiffResult(request, outputStream); } - //~--- fields --------------------------------------------------------------- - - /** implementation of the diff command */ - private final DiffCommand diffCommand; - private Set supportedFeatures; - - /** request for the diff command implementation */ - private final DiffCommandRequest request = new DiffCommandRequest(); + @Override + DiffCommandBuilder self() { + return this; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java new file mode 100644 index 0000000000..d1d223e272 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +public interface DiffFile extends Iterable { + + String getOldRevision(); + + String getNewRevision(); + + String getOldName(); + + String getNewName(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java new file mode 100644 index 0000000000..193e5e75d5 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffLine.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +import java.util.OptionalInt; + +public interface DiffLine { + + OptionalInt getOldLineNumber(); + + OptionalInt getNewLineNumber(); + + String getContent(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java new file mode 100644 index 0000000000..b662db4e2d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResult.java @@ -0,0 +1,8 @@ +package sonia.scm.repository.api; + +public interface DiffResult extends Iterable { + + String getOldRevision(); + + String getNewRevision(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java new file mode 100644 index 0000000000..7e152f3d0f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffResultCommandBuilder.java @@ -0,0 +1,41 @@ +package sonia.scm.repository.api; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.Feature; +import sonia.scm.repository.spi.DiffResultCommand; + +import java.io.IOException; +import java.util.Set; + +public class DiffResultCommandBuilder extends AbstractDiffCommandBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(DiffResultCommandBuilder.class); + + private final DiffResultCommand diffResultCommand; + + DiffResultCommandBuilder(DiffResultCommand diffResultCommand, Set supportedFeatures) { + super(supportedFeatures); + this.diffResultCommand = diffResultCommand; + } + + /** + * Returns the content of the difference as parsed objects. + * + * @return content of the difference + */ + public DiffResult getDiffResult() throws IOException { + Preconditions.checkArgument(request.isValid(), + "path and/or revision is required"); + + LOG.debug("create diff result for {}", request); + + return diffResultCommand.getDiffResult(request); + } + + @Override + DiffResultCommandBuilder self() { + return this; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java new file mode 100644 index 0000000000..6e60f8b2bd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +public interface Hunk extends Iterable { + + int getOldStart(); + + int getOldLineCount(); + + int getNewStart(); + + int getNewLineCount(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index 90978d75ea..c06edcd918 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -239,6 +239,21 @@ public final class RepositoryService implements Closeable { return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures()); } + /** + * The diff command shows differences between revisions for a specified file + * or the entire revision. + * + * @return instance of {@link DiffResultCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + */ + public DiffResultCommandBuilder getDiffResultCommand() { + LOG.debug("create diff result command for repository {}", + repository.getNamespaceAndName()); + + return new DiffResultCommandBuilder(provider.getDiffResultCommand(), provider.getSupportedFeatures()); + } + /** * The incoming command shows new {@link Changeset}s found in a different * repository location. diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java new file mode 100644 index 0000000000..ee50178d76 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/DiffResultCommand.java @@ -0,0 +1,9 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffResult; + +import java.io.IOException; + +public interface DiffResultCommand { + DiffResult getDiffResult(DiffCommandRequest request) throws IOException; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index a82eb7c30a..bf9cdf6a25 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -158,6 +158,11 @@ public abstract class RepositoryServiceProvider implements Closeable throw new CommandNotSupportedException(Command.DIFF); } + public DiffResultCommand getDiffResultCommand() + { + throw new CommandNotSupportedException(Command.DIFF_RESULT); + } + /** * Method description * From 01379caa08d17a3ec4c2fb671a989dec7ac2af96 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 29 Jul 2019 12:54:58 +0200 Subject: [PATCH 067/106] implement first diff details --- .../sonia/scm/repository/api/DiffFile.java | 4 +- .../java/sonia/scm/repository/spi/Differ.java | 113 ++++++++++++++++++ .../repository/spi/GitDiffResultCommand.java | 85 +++++++++++++ .../spi/GitDiffResultCommandTest.java | 42 +++++++ 4 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java create mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java index d1d223e272..a3b1bafe0b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffFile.java @@ -6,7 +6,7 @@ public interface DiffFile extends Iterable { String getNewRevision(); - String getOldName(); + String getOldPath(); - String getNewName(); + String getNewPath(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java new file mode 100644 index 0000000000..67d503aeff --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -0,0 +1,113 @@ +package sonia.scm.repository.spi; + +import com.google.common.base.Strings; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import sonia.scm.repository.GitUtil; +import sonia.scm.util.Util; + +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +final class Differ implements AutoCloseable { + + private final RevWalk walk; + private final TreeWalk treeWalk; + private final RevCommit commit; + + private Differ(RevCommit commit, RevWalk walk, TreeWalk treeWalk) { + this.commit = commit; + this.walk = walk; + this.treeWalk = treeWalk; + } + + public static Differ create(Repository repository, DiffCommandRequest request) throws IOException { + RevWalk walk = new RevWalk(repository); + + ObjectId revision = repository.resolve(request.getRevision()); + RevCommit commit = walk.parseCommit(revision); + + walk.markStart(commit); + commit = walk.next(); + TreeWalk treeWalk = new TreeWalk(repository); + treeWalk.reset(); + treeWalk.setRecursive(true); + + if (Util.isNotEmpty(request.getPath())) + { + treeWalk.setFilter(PathFilter.create(request.getPath())); + } + + + if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) + { + ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); + ObjectId ancestorId = computeCommonAncestor(repository, revision, otherRevision); + RevTree tree = walk.parseCommit(ancestorId).getTree(); + treeWalk.addTree(tree); + } + else if (commit.getParentCount() > 0) + { + RevTree tree = commit.getParent(0).getTree(); + + if (tree != null) + { + treeWalk.addTree(tree); + } + else + { + treeWalk.addTree(new EmptyTreeIterator()); + } + } + else + { + treeWalk.addTree(new EmptyTreeIterator()); + } + + treeWalk.addTree(commit.getTree()); + + return new Differ(commit, walk, treeWalk); + } + + private static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { + return GitUtil.computeCommonAncestor(repository, revision1, revision2); + } + + public void process(Consumer diffConsumer) throws IOException { + List entries = DiffEntry.scan(treeWalk); + diffConsumer.accept(new Diff(commit, entries)); + } + + @Override + public void close() { + GitUtil.release(walk); + GitUtil.release(treeWalk); + } + + public static class Diff { + + private final RevCommit commit; + private final List entries; + + private Diff(RevCommit commit, List entries) { + this.commit = commit; + this.entries = entries; + } + + public RevCommit getCommit() { + return commit; + } + + public List getEntries() { + return entries; + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java new file mode 100644 index 0000000000..a7c8cc3f84 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java @@ -0,0 +1,85 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.diff.DiffEntry; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.DiffFile; +import sonia.scm.repository.api.DiffResult; +import sonia.scm.repository.api.Hunk; + +import java.io.IOException; +import java.util.Iterator; +import java.util.stream.Collectors; + +public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand { + + GitDiffResultCommand(GitContext context, Repository repository) { + super(context, repository); + } + + public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException { + try (Differ differ = Differ.create(open(), diffCommandRequest)) { + GitDiffResult result = new GitDiffResult(); + differ.process(result::process); + return result; + } + } + + private class GitDiffResult implements DiffResult { + + private Differ.Diff diff; + + void process(Differ.Diff diff) { + this.diff = diff; + } + + @Override + public String getOldRevision() { + return GitUtil.getId(diff.getCommit().getParent(0).getId()); + } + + @Override + public String getNewRevision() { + return GitUtil.getId(diff.getCommit().getId()); + } + + @Override + public Iterator iterator() { + return diff.getEntries().stream().map(GitDiffFile::new).collect(Collectors.toList()).iterator(); + } + } + + private static class GitDiffFile implements DiffFile { + + private final DiffEntry diffEntry; + + private GitDiffFile(DiffEntry diffEntry) { + this.diffEntry = diffEntry; + } + + @Override + public String getOldRevision() { + return null; + } + + @Override + public String getNewRevision() { + return null; + } + + @Override + public String getOldPath() { + return diffEntry.getOldPath(); + } + + @Override + public String getNewPath() { + return diffEntry.getNewPath(); + } + + @Override + public Iterator iterator() { + return null; + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java new file mode 100644 index 0000000000..a8b90d2b5c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java @@ -0,0 +1,42 @@ +package sonia.scm.repository.spi; + +import org.junit.Test; +import sonia.scm.repository.api.DiffFile; +import sonia.scm.repository.api.DiffResult; + +import java.io.IOException; +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GitDiffResultCommandTest extends AbstractGitCommandTestBase { + + @Test + public void shouldReturnOldAndNewRevision() throws IOException { + GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + + DiffResult diffResult = gitDiffResultCommand.getDiffResult(diffCommandRequest); + + assertThat(diffResult.getNewRevision()).isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + assertThat(diffResult.getOldRevision()).isEqualTo("592d797cd36432e591416e8b2b98154f4f163411"); + } + + @Test + public void shouldReturnFilePaths() throws IOException { + GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + + DiffResult diffResult = gitDiffResultCommand.getDiffResult(diffCommandRequest); + Iterator iterator = diffResult.iterator(); + DiffFile a = iterator.next(); + assertThat(a.getNewPath()).isEqualTo("a.txt"); + assertThat(a.getOldPath()).isEqualTo("a.txt"); + + DiffFile b = iterator.next(); + assertThat(b.getOldPath()).isEqualTo("b.txt"); + assertThat(b.getNewPath()).isEqualTo("/dev/null"); + } +} From 07068880bb702b495d3967245b536d13c150d808 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 29 Jul 2019 16:42:49 +0200 Subject: [PATCH 068/106] implemented parsing of git diff hunks --- .../java/sonia/scm/repository/GitUtil.java | 3 +- .../sonia/scm/repository/spi/FileRange.java | 20 ++ .../repository/spi/GitDiffResultCommand.java | 42 ++++- .../sonia/scm/repository/spi/GitHunk.java | 48 +++++ .../scm/repository/spi/GitHunkParser.java | 175 ++++++++++++++++++ .../spi/GitDiffResultCommandTest.java | 67 ++++++- .../scm/repository/spi/GitHunkParserTest.java | 138 ++++++++++++++ 7 files changed, 474 insertions(+), 19 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java create mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 7aacdb256a..7175d3b646 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -43,6 +43,7 @@ import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -441,7 +442,7 @@ public final class GitUtil * * @return */ - public static String getId(ObjectId objectId) + public static String getId(AnyObjectId objectId) { String id = Util.EMPTY_STRING; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java new file mode 100644 index 0000000000..8d445e1c44 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/FileRange.java @@ -0,0 +1,20 @@ +package sonia.scm.repository.spi; + +public class FileRange { + + private final int start; + private final int lineCount; + + public FileRange(int start, int lineCount) { + this.start = start; + this.lineCount = lineCount; + } + + public int getStart() { + return start; + } + + public int getLineCount() { + return lineCount; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java index a7c8cc3f84..de7c8483f5 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java @@ -1,12 +1,15 @@ package sonia.scm.repository.spi; +import com.google.common.base.Throwables; import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.api.DiffFile; import sonia.scm.repository.api.DiffResult; import sonia.scm.repository.api.Hunk; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Iterator; import java.util.stream.Collectors; @@ -18,8 +21,9 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu } public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException { - try (Differ differ = Differ.create(open(), diffCommandRequest)) { - GitDiffResult result = new GitDiffResult(); + org.eclipse.jgit.lib.Repository repository = open(); + try (Differ differ = Differ.create(repository, diffCommandRequest)) { + GitDiffResult result = new GitDiffResult(repository); differ.process(result::process); return result; } @@ -27,8 +31,13 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu private class GitDiffResult implements DiffResult { + private final org.eclipse.jgit.lib.Repository repository; private Differ.Diff diff; + private GitDiffResult(org.eclipse.jgit.lib.Repository repository) { + this.repository = repository; + } + void process(Differ.Diff diff) { this.diff = diff; } @@ -45,26 +54,28 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu @Override public Iterator iterator() { - return diff.getEntries().stream().map(GitDiffFile::new).collect(Collectors.toList()).iterator(); + return diff.getEntries().stream().map(diffEntry -> new GitDiffFile(repository, diffEntry)).collect(Collectors.toList()).iterator(); } } - private static class GitDiffFile implements DiffFile { + private class GitDiffFile implements DiffFile { + private final org.eclipse.jgit.lib.Repository repository; private final DiffEntry diffEntry; - private GitDiffFile(DiffEntry diffEntry) { + private GitDiffFile(org.eclipse.jgit.lib.Repository repository, DiffEntry diffEntry) { + this.repository = repository; this.diffEntry = diffEntry; } @Override public String getOldRevision() { - return null; + return GitUtil.getId(diffEntry.getOldId().toObjectId()); } @Override public String getNewRevision() { - return null; + return GitUtil.getId(diffEntry.getNewId().toObjectId()); } @Override @@ -79,7 +90,22 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu @Override public Iterator iterator() { - return null; + String content = format(repository, diffEntry); + GitHunkParser parser = new GitHunkParser(); + return parser.parse(content).iterator(); } + + private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) { + try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + DiffFormatter formatter = new DiffFormatter(baos); + formatter.setRepository(repository); + formatter.format(entry); + return baos.toString(); + } catch (IOException ex) { + throw Throwables.propagate(ex); + } + } + } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java new file mode 100644 index 0000000000..9a272d7b10 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunk.java @@ -0,0 +1,48 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.Iterator; +import java.util.List; + +public class GitHunk implements Hunk { + + private final FileRange oldFileRange; + private final FileRange newFileRange; + private List lines; + + public GitHunk(FileRange oldFileRange, FileRange newFileRange) { + this.oldFileRange = oldFileRange; + this.newFileRange = newFileRange; + } + + @Override + public int getOldStart() { + return oldFileRange.getStart(); + } + + @Override + public int getOldLineCount() { + return oldFileRange.getLineCount(); + } + + @Override + public int getNewStart() { + return newFileRange.getStart(); + } + + @Override + public int getNewLineCount() { + return newFileRange.getLineCount(); + } + + @Override + public Iterator iterator() { + return lines.iterator(); + } + + void setLines(List lines) { + this.lines = lines; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java new file mode 100644 index 0000000000..caedc6605f --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java @@ -0,0 +1,175 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.Scanner; + +import static java.util.OptionalInt.of; + +final class GitHunkParser { + private static final int HEADER_PREFIX_LENGTH = "@@ -".length(); + private static final int HEADER_SUFFIX_LENGTH = " @@".length(); + + private GitHunk currentGitHunk = null; + private List collectedLines = null; + private int oldLineCounter = 0; + private int newLineCounter = 0; + + GitHunkParser() { + } + + public List parse(String content) { + List hunks = new ArrayList<>(); + + try (Scanner scanner = new Scanner(content)) { + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.startsWith("@@")) { + parseHeader(hunks, line); + } else if (currentGitHunk != null) { + parseDiffLine(line); + } + } + } + if (currentGitHunk != null) { + currentGitHunk.setLines(collectedLines); + } + + return hunks; + } + + private void parseHeader(List hunks, String line) { + if (currentGitHunk != null) { + currentGitHunk.setLines(collectedLines); + } + String hunkHeader = line.substring(HEADER_PREFIX_LENGTH, line.length() - HEADER_SUFFIX_LENGTH); + String[] split = hunkHeader.split("\\s"); + + FileRange oldFileRange = createFileRange(split[0]); + // TODO merge contains two two block which starts with "-" e.g. -1,3 -2,4 +3,6 + FileRange newFileRange = createFileRange(split[1]); + + currentGitHunk = new GitHunk(oldFileRange, newFileRange); + hunks.add(currentGitHunk); + + collectedLines = new ArrayList<>(); + oldLineCounter = currentGitHunk.getOldStart(); + newLineCounter = currentGitHunk.getNewStart(); + } + + private void parseDiffLine(String line) { + String content = line.substring(1); + switch (line.charAt(0)) { + case ' ': + collectedLines.add(new UnchangedGitDiffLine(newLineCounter, oldLineCounter, content)); + ++newLineCounter; + ++oldLineCounter; + break; + case '+': + collectedLines.add(new AddedGitDiffLine(newLineCounter, content)); + ++newLineCounter; + break; + case '-': + collectedLines.add(new RemovedGitDiffLine(oldLineCounter, content)); + ++oldLineCounter; + break; + default: + throw new IllegalStateException("cannot handle diff line: " + line); + } + } + + private static class AddedGitDiffLine implements DiffLine { + private final int newLineNumber; + private final String content; + + private AddedGitDiffLine(int newLineNumber, String content) { + this.newLineNumber = newLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return OptionalInt.empty(); + } + + @Override + public OptionalInt getNewLineNumber() { + return of(newLineNumber); + } + + @Override + public String getContent() { + return content; + } + } + + private static class RemovedGitDiffLine implements DiffLine { + private final int oldLineNumber; + private final String content; + + private RemovedGitDiffLine(int oldLineNumber, String content) { + this.oldLineNumber = oldLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return of(oldLineNumber); + } + + @Override + public OptionalInt getNewLineNumber() { + return OptionalInt.empty(); + } + + @Override + public String getContent() { + return content; + } + } + + private static class UnchangedGitDiffLine implements DiffLine { + private final int newLineNumber; + private final int oldLineNumber; + private final String content; + + public UnchangedGitDiffLine(int newLineNumber, int oldLineNumber, String content) { + this.newLineNumber = newLineNumber; + this.oldLineNumber = oldLineNumber; + this.content = content; + } + + @Override + public OptionalInt getOldLineNumber() { + return of(oldLineNumber); + } + + @Override + public OptionalInt getNewLineNumber() { + return of(newLineNumber); + } + + @Override + public String getContent() { + return content; + } + } + + private static FileRange createFileRange(String fileRangeString) { + int start; + int lineCount = 1; + int commaIndex = fileRangeString.indexOf(','); + if (commaIndex > 0) { + start = Integer.parseInt(fileRangeString.substring(0, commaIndex)); + lineCount = Integer.parseInt(fileRangeString.substring(commaIndex + 1)); + } else { + start = Integer.parseInt(fileRangeString); + } + + return new FileRange(start, lineCount); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java index a8b90d2b5c..f359ae987c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffResultCommandTest.java @@ -3,6 +3,7 @@ package sonia.scm.repository.spi; import org.junit.Test; import sonia.scm.repository.api.DiffFile; import sonia.scm.repository.api.DiffResult; +import sonia.scm.repository.api.Hunk; import java.io.IOException; import java.util.Iterator; @@ -13,11 +14,7 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase { @Test public void shouldReturnOldAndNewRevision() throws IOException { - GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository); - DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); - diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); - - DiffResult diffResult = gitDiffResultCommand.getDiffResult(diffCommandRequest); + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); assertThat(diffResult.getNewRevision()).isEqualTo("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); assertThat(diffResult.getOldRevision()).isEqualTo("592d797cd36432e591416e8b2b98154f4f163411"); @@ -25,11 +22,7 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase { @Test public void shouldReturnFilePaths() throws IOException { - GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository); - DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); - diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); - - DiffResult diffResult = gitDiffResultCommand.getDiffResult(diffCommandRequest); + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); Iterator iterator = diffResult.iterator(); DiffFile a = iterator.next(); assertThat(a.getNewPath()).isEqualTo("a.txt"); @@ -39,4 +32,58 @@ public class GitDiffResultCommandTest extends AbstractGitCommandTestBase { assertThat(b.getOldPath()).isEqualTo("b.txt"); assertThat(b.getNewPath()).isEqualTo("/dev/null"); } + + @Test + public void shouldReturnFileRevisions() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + assertThat(a.getOldRevision()).isEqualTo("78981922613b2afb6025042ff6bd878ac1994e85"); + assertThat(a.getNewRevision()).isEqualTo("1dc60c7504f4326bc83b9b628c384ec8d7e57096"); + + DiffFile b = iterator.next(); + assertThat(b.getOldRevision()).isEqualTo("61780798228d17af2d34fce4cfbdf35556832472"); + assertThat(b.getNewRevision()).isEqualTo("0000000000000000000000000000000000000000"); + } + + @Test + public void shouldReturnFileHunks() throws IOException { + DiffResult diffResult = createDiffResult("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + Iterator hunks = a.iterator(); + + Hunk hunk = hunks.next(); + assertThat(hunk.getOldStart()).isEqualTo(1); + assertThat(hunk.getOldLineCount()).isEqualTo(1); + + assertThat(hunk.getNewStart()).isEqualTo(1); + assertThat(hunk.getNewLineCount()).isEqualTo(1); + } + + @Test + public void shouldReturnFileHunksWithFullFileRange() throws IOException { + DiffResult diffResult = createDiffResult("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + Iterator iterator = diffResult.iterator(); + + DiffFile a = iterator.next(); + Iterator hunks = a.iterator(); + + Hunk hunk = hunks.next(); + assertThat(hunk.getOldStart()).isEqualTo(1); + assertThat(hunk.getOldLineCount()).isEqualTo(1); + + assertThat(hunk.getNewStart()).isEqualTo(1); + assertThat(hunk.getNewLineCount()).isEqualTo(2); + } + + private DiffResult createDiffResult(String s) throws IOException { + GitDiffResultCommand gitDiffResultCommand = new GitDiffResultCommand(createContext(), repository); + DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); + diffCommandRequest.setRevision(s); + + return gitDiffResultCommand.getDiffResult(diffCommandRequest); + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java new file mode 100644 index 0000000000..a58fae644d --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java @@ -0,0 +1,138 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.api.DiffLine; +import sonia.scm.repository.api.Hunk; + +import java.util.Iterator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class GitHunkParserTest { + + private static final String DIFF_001 = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1 +1,2 @@\n" + + " a\n" + + "+added line\n"; + + private static final String DIFF_002 = "diff --git a/file b/file\n" + + "index 5e89957..e8823e1 100644\n" + + "--- a/file\n" + + "+++ b/file\n" + + "@@ -2,6 +2,9 @@\n" + + " 2\n" + + " 3\n" + + " 4\n" + + "+5\n" + + "+6\n" + + "+7\n" + + " 8\n" + + " 9\n" + + " 10\n" + + "@@ -15,14 +18,13 @@\n" + + " 18\n" + + " 19\n" + + " 20\n" + + "+21\n" + + "+22\n" + + " 23\n" + + " 24\n" + + " 25\n" + + " 26\n" + + " 27\n" + + "-a\n" + + "-b\n" + + "-c\n" + + " 28\n" + + " 29\n" + + " 30"; + + private static final String DIFF_003 = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1,2 +1 @@\n" + + " a\n" + + "-removed line\n"; + + private static final String ILLEGAL_DIFF = "diff --git a/a.txt b/a.txt\n" + + "index 7898192..2f8bc28 100644\n" + + "--- a/a.txt\n" + + "+++ b/a.txt\n" + + "@@ -1,2 +1 @@\n" + + " a\n" + + "~illegal line\n"; + + @Test + void shouldParseHunks() { + List hunks = new GitHunkParser().parse(DIFF_001); + assertThat(hunks).hasSize(1); + assertHunk(hunks.get(0), 1, 1, 1, 2); + } + + @Test + void shouldParseMultipleHunks() { + List hunks = new GitHunkParser().parse(DIFF_002); + + assertThat(hunks).hasSize(2); + assertHunk(hunks.get(0), 2, 6, 2, 9); + assertHunk(hunks.get(1), 15, 14, 18, 13); + } + + @Test + void shouldParseAddedHunkLines() { + List hunks = new GitHunkParser().parse(DIFF_001); + + Hunk hunk = hunks.get(0); + + Iterator lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(1); + assertThat(line1.getNewLineNumber()).hasValue(1); + assertThat(line1.getContent()).isEqualTo("a"); + + DiffLine line2 = lines.next(); + assertThat(line2.getOldLineNumber()).isEmpty(); + assertThat(line2.getNewLineNumber()).hasValue(2); + assertThat(line2.getContent()).isEqualTo("added line"); + } + + @Test + void shouldParseRemovedHunkLines() { + List hunks = new GitHunkParser().parse(DIFF_003); + + Hunk hunk = hunks.get(0); + + Iterator lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(1); + assertThat(line1.getNewLineNumber()).hasValue(1); + assertThat(line1.getContent()).isEqualTo("a"); + + DiffLine line2 = lines.next(); + assertThat(line2.getOldLineNumber()).hasValue(2); + assertThat(line2.getNewLineNumber()).isEmpty(); + assertThat(line2.getContent()).isEqualTo("removed line"); + } + + @Test + void shouldFailForIllegalLine() { + assertThrows(IllegalStateException.class, () -> new GitHunkParser().parse(ILLEGAL_DIFF)); + } + + private void assertHunk(Hunk hunk, int oldStart, int oldLineCount, int newStart, int newLineCount) { + assertThat(hunk.getOldStart()).isEqualTo(oldStart); + assertThat(hunk.getOldLineCount()).isEqualTo(oldLineCount); + + assertThat(hunk.getNewStart()).isEqualTo(newStart); + assertThat(hunk.getNewLineCount()).isEqualTo(newLineCount); + } + +} From e3787fd764f92cd3ee9d104870d83d3b241e9cfd Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 30 Jul 2019 08:06:10 +0200 Subject: [PATCH 069/106] simplify Differ api and use the new api in GitDiffCommand --- .../java/sonia/scm/repository/spi/Differ.java | 13 ++-- .../scm/repository/spi/GitDiffCommand.java | 72 ++----------------- .../repository/spi/GitDiffResultCommand.java | 15 ++-- .../scm/repository/spi/GitHunkParser.java | 3 +- 4 files changed, 20 insertions(+), 83 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java index 67d503aeff..ca417550f4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -15,7 +15,6 @@ import sonia.scm.util.Util; import java.io.IOException; import java.util.List; -import java.util.function.Consumer; final class Differ implements AutoCloseable { @@ -29,7 +28,13 @@ final class Differ implements AutoCloseable { this.treeWalk = treeWalk; } - public static Differ create(Repository repository, DiffCommandRequest request) throws IOException { + static Diff diff(Repository repository, DiffCommandRequest request) throws IOException { + try (Differ differ = create(repository, request)) { + return differ.diff(); + } + } + + private static Differ create(Repository repository, DiffCommandRequest request) throws IOException { RevWalk walk = new RevWalk(repository); ObjectId revision = repository.resolve(request.getRevision()); @@ -81,9 +86,9 @@ final class Differ implements AutoCloseable { return GitUtil.computeCommonAncestor(repository, revision1, revision2); } - public void process(Consumer diffConsumer) throws IOException { + private Diff diff() throws IOException { List entries = DiffEntry.scan(treeWalk); - diffConsumer.accept(new Diff(commit, entries)); + return new Diff(commit, entries); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 2d56c8e786..7d5e45a5c2 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -34,26 +34,15 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Strings; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevTree; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.treewalk.EmptyTreeIterator; -import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.filter.PathFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; -import sonia.scm.util.Util; import java.io.BufferedOutputStream; -import java.io.IOException; import java.io.OutputStream; -import java.util.List; /** * @@ -78,7 +67,7 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand * @param context * @param repository */ - public GitDiffCommand(GitContext context, Repository repository) + GitDiffCommand(GitContext context, Repository repository) { super(context, repository); } @@ -95,63 +84,18 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand @Override public void getDiffResult(DiffCommandRequest request, OutputStream output) { - RevWalk walk = null; - TreeWalk treeWalk = null; DiffFormatter formatter = null; try { - org.eclipse.jgit.lib.Repository gr = open(); + org.eclipse.jgit.lib.Repository repository = open(); - walk = new RevWalk(gr); - - ObjectId revision = gr.resolve(request.getRevision()); - RevCommit commit = walk.parseCommit(revision); - - walk.markStart(commit); - commit = walk.next(); - treeWalk = new TreeWalk(gr); - treeWalk.reset(); - treeWalk.setRecursive(true); - - if (Util.isNotEmpty(request.getPath())) - { - treeWalk.setFilter(PathFilter.create(request.getPath())); - } - - - if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) - { - ObjectId otherRevision = gr.resolve(request.getAncestorChangeset()); - ObjectId ancestorId = computeCommonAncestor(gr, revision, otherRevision); - RevTree tree = walk.parseCommit(ancestorId).getTree(); - treeWalk.addTree(tree); - } - else if (commit.getParentCount() > 0) - { - RevTree tree = commit.getParent(0).getTree(); - - if (tree != null) - { - treeWalk.addTree(tree); - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } - } - else - { - treeWalk.addTree(new EmptyTreeIterator()); - } - - treeWalk.addTree(commit.getTree()); formatter = new DiffFormatter(new BufferedOutputStream(output)); - formatter.setRepository(gr); + formatter.setRepository(repository); - List entries = DiffEntry.scan(treeWalk); + Differ.Diff diff = Differ.diff(repository, request); - for (DiffEntry e : entries) + for (DiffEntry e : diff.getEntries()) { if (!e.getOldId().equals(e.getNewId())) { @@ -168,14 +112,8 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand } finally { - GitUtil.release(walk); - GitUtil.release(treeWalk); GitUtil.release(formatter); } } - private ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { - return GitUtil.computeCommonAncestor(repository, revision1, revision2); - } - } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java index de7c8483f5..0d5f4f7b9e 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java @@ -22,23 +22,16 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException { org.eclipse.jgit.lib.Repository repository = open(); - try (Differ differ = Differ.create(repository, diffCommandRequest)) { - GitDiffResult result = new GitDiffResult(repository); - differ.process(result::process); - return result; - } + return new GitDiffResult(repository, Differ.diff(repository, diffCommandRequest)); } private class GitDiffResult implements DiffResult { private final org.eclipse.jgit.lib.Repository repository; - private Differ.Diff diff; + private final Differ.Diff diff; - private GitDiffResult(org.eclipse.jgit.lib.Repository repository) { + private GitDiffResult(org.eclipse.jgit.lib.Repository repository, Differ.Diff diff) { this.repository = repository; - } - - void process(Differ.Diff diff) { this.diff = diff; } @@ -96,7 +89,7 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu } private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) { - try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { DiffFormatter formatter = new DiffFormatter(baos); formatter.setRepository(repository); formatter.format(entry); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java index caedc6605f..b7594bb5d6 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java @@ -51,6 +51,7 @@ final class GitHunkParser { FileRange oldFileRange = createFileRange(split[0]); // TODO merge contains two two block which starts with "-" e.g. -1,3 -2,4 +3,6 + // check if it is relevant for our use case FileRange newFileRange = createFileRange(split[1]); currentGitHunk = new GitHunk(oldFileRange, newFileRange); @@ -137,7 +138,7 @@ final class GitHunkParser { private final int oldLineNumber; private final String content; - public UnchangedGitDiffLine(int newLineNumber, int oldLineNumber, String content) { + private UnchangedGitDiffLine(int newLineNumber, int oldLineNumber, String content) { this.newLineNumber = newLineNumber; this.oldLineNumber = oldLineNumber; this.content = content; From 0b76cb7ea5b5af07544b76d37f2df4d9e84f2547 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 30 Jul 2019 10:00:36 +0200 Subject: [PATCH 070/106] added raw header to hunk --- .../java/sonia/scm/repository/api/Hunk.java | 12 +++++ .../sonia/scm/repository/api/HunkTest.java | 53 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java index 6e60f8b2bd..c8a3e1ebca 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Hunk.java @@ -2,6 +2,18 @@ package sonia.scm.repository.api; public interface Hunk extends Iterable { + default String getRawHeader() { + return String.format("@@ -%s +%s @@", getLineMarker(getOldStart(), getOldLineCount()), getLineMarker(getNewStart(), getNewLineCount())); + } + + default String getLineMarker(int start, int lineCount) { + if (lineCount == 1) { + return Integer.toString(start); + } else { + return String.format("%s,%s", start, lineCount); + } + } + int getOldStart(); int getOldLineCount(); diff --git a/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java b/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java new file mode 100644 index 0000000000..086df81741 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/HunkTest.java @@ -0,0 +1,53 @@ +package sonia.scm.repository.api; + +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +class HunkTest { + + @Test + void shouldGetComplexHeader() { + String rawHeader = createHunk(2, 3, 4, 5).getRawHeader(); + + assertThat(rawHeader).isEqualTo("@@ -2,3 +4,5 @@"); + } + + @Test + void shouldReturnSingleNumberForOne() { + String rawHeader = createHunk(42, 1, 5, 1).getRawHeader(); + + assertThat(rawHeader).isEqualTo("@@ -42 +5 @@"); + } + + private Hunk createHunk(int oldStart, int oldLineCount, int newStart, int newLineCount) { + return new Hunk() { + @Override + public int getOldStart() { + return oldStart; + } + + @Override + public int getOldLineCount() { + return oldLineCount; + } + + @Override + public int getNewStart() { + return newStart; + } + + @Override + public int getNewLineCount() { + return newLineCount; + } + + @Override + public Iterator iterator() { + return null; + } + }; + } +} From bbdc5a198908507d577a515103a95a2d8a002535 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 30 Jul 2019 10:01:00 +0200 Subject: [PATCH 071/106] fix Diff ui types --- .../packages/ui-components/src/repos/Diff.js | 4 ++-- .../packages/ui-components/src/repos/DiffTypes.js | 13 +++++++++---- .../packages/ui-components/src/repos/LoadingDiff.js | 7 ++++--- .../packages/ui-components/src/repos/index.js | 1 + 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/repos/Diff.js b/scm-ui-components/packages/ui-components/src/repos/Diff.js index 369e812344..97692210c2 100644 --- a/scm-ui-components/packages/ui-components/src/repos/Diff.js +++ b/scm-ui-components/packages/ui-components/src/repos/Diff.js @@ -1,10 +1,10 @@ //@flow import React from "react"; import DiffFile from "./DiffFile"; -import type { DiffObjectProps } from "./DiffTypes"; +import type { DiffObjectProps, File } from "./DiffTypes"; type Props = DiffObjectProps & { - diff: any + diff: File[] }; class Diff extends React.Component { diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js b/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js index 74803e0a4e..dcd23bc9a8 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffTypes.js @@ -27,12 +27,17 @@ export type Hunk = { content: string }; +export type ChangeType = "insert" | "delete" | "normal"; + export type Change = { content: string, - isNormal: boolean, - newLineNumber: number, - oldLineNumber: number, - type: string + isNormal?: boolean, + isInsert?: boolean, + isDelete?: boolean, + lineNumber?: number, + newLineNumber?: number, + oldLineNumber?: number, + type: ChangeType }; export type BaseContext = { diff --git a/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js b/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js index ce1a074b41..c8a5250756 100644 --- a/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js +++ b/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js @@ -6,14 +6,14 @@ import parser from "gitdiff-parser"; import Loading from "../Loading"; import Diff from "./Diff"; -import type {DiffObjectProps} from "./DiffTypes"; +import type {DiffObjectProps, File} from "./DiffTypes"; type Props = DiffObjectProps & { url: string }; type State = { - diff?: any, + diff?: File[], loading: boolean, error?: Error }; @@ -47,7 +47,8 @@ class LoadingDiff extends React.Component { .get(url) .then(response => response.text()) .then(parser.parse) - .then(diff => { + // $FlowFixMe + .then((diff: File[]) => { this.setState({ loading: false, diff: diff diff --git a/scm-ui-components/packages/ui-components/src/repos/index.js b/scm-ui-components/packages/ui-components/src/repos/index.js index 473bbb3efc..fd1448fbdd 100644 --- a/scm-ui-components/packages/ui-components/src/repos/index.js +++ b/scm-ui-components/packages/ui-components/src/repos/index.js @@ -12,6 +12,7 @@ export type { FileChangeType, Hunk, Change, + ChangeType, BaseContext, AnnotationFactory, AnnotationFactoryContext, From 00fa943e51f574597149be607a60a7bb55dbd01f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 31 Jul 2019 13:24:31 +0200 Subject: [PATCH 072/106] fix SonarQube issues --- .../scm/repository/spi/GitDiffCommand.java | 82 ++++--------------- .../repository/spi/GitDiffResultCommand.java | 12 ++- 2 files changed, 26 insertions(+), 68 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 7d5e45a5c2..5d5f27806b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -1,19 +1,19 @@ /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. - * + *

    * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + *

    * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + *

    * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,9 +24,8 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + *

    * http://bitbucket.org/sdorra/scm-manager - * */ @@ -36,84 +35,39 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; import java.io.BufferedOutputStream; +import java.io.IOException; import java.io.OutputStream; /** * * @author Sebastian Sdorra */ -public class GitDiffCommand extends AbstractGitCommand implements DiffCommand -{ +public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { - /** - * the logger for GitDiffCommand - */ - private static final Logger logger = - LoggerFactory.getLogger(GitDiffCommand.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param context - * @param repository - */ - GitDiffCommand(GitContext context, Repository repository) - { + GitDiffCommand(GitContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - */ @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) - { - DiffFormatter formatter = null; - - try - { - org.eclipse.jgit.lib.Repository repository = open(); - - formatter = new DiffFormatter(new BufferedOutputStream(output)); + public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException { + @SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService + org.eclipse.jgit.lib.Repository repository = open(); + try (DiffFormatter formatter = new DiffFormatter(new BufferedOutputStream(output))) { formatter.setRepository(repository); Differ.Diff diff = Differ.diff(repository, request); - for (DiffEntry e : diff.getEntries()) - { - if (!e.getOldId().equals(e.getNewId())) - { + for (DiffEntry e : diff.getEntries()) { + if (!e.getOldId().equals(e.getNewId())) { formatter.format(e); } } formatter.flush(); } - catch (Exception ex) - { - // TODO throw exception - logger.error("could not create diff", ex); - } - finally - { - GitUtil.release(formatter); - } } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java index 0d5f4f7b9e..a3f63b8f5a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffResultCommand.java @@ -4,6 +4,7 @@ import com.google.common.base.Throwables; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.api.DiffFile; import sonia.scm.repository.api.DiffResult; @@ -47,7 +48,11 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu @Override public Iterator iterator() { - return diff.getEntries().stream().map(diffEntry -> new GitDiffFile(repository, diffEntry)).collect(Collectors.toList()).iterator(); + return diff.getEntries() + .stream() + .map(diffEntry -> new GitDiffFile(repository, diffEntry)) + .collect(Collectors.toList()) + .iterator(); } } @@ -89,13 +94,12 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu } private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - DiffFormatter formatter = new DiffFormatter(baos); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DiffFormatter formatter = new DiffFormatter(baos)) { formatter.setRepository(repository); formatter.format(entry); return baos.toString(); } catch (IOException ex) { - throw Throwables.propagate(ex); + throw new InternalRepositoryException(GitDiffResultCommand.this.repository, "failed to format diff entry", ex); } } From f6fd25cbc383bf85505d052bddbd3a26f78acd77 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 31 Jul 2019 13:26:58 +0200 Subject: [PATCH 073/106] reactivate jenkins lifecycle stage --- Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0f586bf346..e817ac2ba1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -51,9 +51,9 @@ node('docker') { if (isMainBranch()) { -// stage('Lifecycle') { -// nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' -// } + stage('Lifecycle') { + nexusPolicyEvaluation iqApplication: selectedApplication('scm'), iqScanPatterns: [[scanPattern: 'scm-server/target/scm-server-app.zip']], iqStage: 'build' + } stage('Archive') { archiveArtifacts 'scm-webapp/target/scm-webapp.war' From f10b653a1d2318ae23979df436c3242e2c30f07b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 31 Jul 2019 13:37:12 +0200 Subject: [PATCH 074/106] Fix thrown exceptions --- .../sonia/scm/repository/spi/GitDiffCommandTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index f6e462f968..fd9c45be5c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -3,6 +3,7 @@ package sonia.scm.repository.spi; import org.junit.Test; import java.io.ByteArrayOutputStream; +import java.io.IOException; import static org.junit.Assert.assertEquals; @@ -38,7 +39,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { "+f\n"; @Test - public void diffForOneRevisionShouldCreateDiff() { + public void diffForOneRevisionShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); @@ -48,7 +49,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffForOneBranchShouldCreateDiff() { + public void diffForOneBranchShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); @@ -58,7 +59,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffForPathShouldCreateLimitedDiff() { + public void diffForPathShouldCreateLimitedDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); @@ -69,7 +70,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffBetweenTwoBranchesShouldCreateDiff() { + public void diffBetweenTwoBranchesShouldCreateDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("master"); @@ -80,7 +81,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { } @Test - public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() { + public void diffBetweenTwoBranchesForPathShouldCreateLimitedDiff() throws IOException { GitDiffCommand gitDiffCommand = new GitDiffCommand(createContext(), repository); DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("master"); From 6caf01f4f6db232c45eda4ae30854cd4cba77460 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 31 Jul 2019 17:14:34 +0200 Subject: [PATCH 075/106] close branch feature/parsed_diff From 86af7b23eb5e54780183014d1d2aca4eda4e2f63 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 1 Aug 2019 15:43:12 +0200 Subject: [PATCH 076/106] remove groups from BearerRealm / SyncRealmHelper / DAORealmHelper --- .../sonia/scm/security/DAORealmHelper.java | 36 ++++++------- .../scm/security/DAORealmHelperFactory.java | 14 +++-- .../sonia/scm/security/GroupCollector.java | 33 ++++++++++-- .../sonia/scm/security/GroupResolver.java | 9 ++++ .../scm/security/SyncingRealmHelper.java | 8 +-- .../scm/security/DAORealmHelperTest.java | 27 +--------- .../scm/security/SyncingRealmHelperTest.java | 54 +++++-------------- .../java/sonia/scm/security/BearerRealm.java | 1 - .../sonia/scm/security/BearerRealmTest.java | 2 - 9 files changed, 80 insertions(+), 104 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/security/GroupResolver.java diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java index ea3e7ce9f5..6ec64a67de 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java @@ -45,7 +45,6 @@ import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.subject.SimplePrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.group.GroupDAO; import sonia.scm.user.User; import sonia.scm.user.UserDAO; @@ -71,8 +70,6 @@ public final class DAORealmHelper { private final UserDAO userDAO; - private final GroupCollector groupCollector; - private final String realm; //~--- constructors --------------------------------------------------------- @@ -83,14 +80,12 @@ public final class DAORealmHelper { * * @param loginAttemptHandler login attempt handler for wrapping credentials matcher * @param userDAO user dao - * @param groupCollector collect groups for a principal * @param realm name of realm */ - public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupCollector groupCollector, String realm) { + public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, String realm) { this.loginAttemptHandler = loginAttemptHandler; this.realm = realm; this.userDAO = userDAO; - this.groupCollector = groupCollector; } //~--- get methods ---------------------------------------------------------- @@ -120,7 +115,7 @@ public final class DAORealmHelper { UsernamePasswordToken upt = (UsernamePasswordToken) token; String principal = upt.getUsername(); - return getAuthenticationInfo(principal, null, null, Collections.emptySet()); + return getAuthenticationInfo(principal, null, null); } /** @@ -135,7 +130,7 @@ public final class DAORealmHelper { } - private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope, Iterable groups) { + private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) { checkArgument(!Strings.isNullOrEmpty(principal), "username is required"); LOG.debug("try to authenticate {}", principal); @@ -153,7 +148,6 @@ public final class DAORealmHelper { collection.add(principal, realm); collection.add(user, realm); - collection.add(groupCollector.collect(principal, groups), realm); collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm); String creds = credentials; @@ -207,17 +201,17 @@ public final class DAORealmHelper { return this; } - /** - * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info. - * - * @param groups extra groups - * - * @return {@code this} - */ - public AuthenticationInfoBuilder withGroups(Iterable groups) { - this.groups = groups; - return this; - } +// /** +// * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info. +// * +// * @param groups extra groups +// * +// * @return {@code this} +// */ +// public AuthenticationInfoBuilder withGroups(Iterable groups) { +// this.groups = groups; +// return this; +// } /** * Build creates the authentication info from the given information. @@ -225,7 +219,7 @@ public final class DAORealmHelper { * @return authentication info */ public AuthenticationInfo build() { - return getAuthenticationInfo(principal, credentials, scope, groups); + return getAuthenticationInfo(principal, credentials, scope); } } diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java index ee2bf11e21..b503ff8375 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java @@ -30,6 +30,7 @@ */ package sonia.scm.security; +import sonia.scm.cache.CacheManager; import sonia.scm.group.GroupDAO; import sonia.scm.user.UserDAO; @@ -45,20 +46,23 @@ public final class DAORealmHelperFactory { private final LoginAttemptHandler loginAttemptHandler; private final UserDAO userDAO; - private final GroupCollector groupCollector; + private final CacheManager cacheManager; + private final GroupResolver groupResolver; /** * Constructs a new instance. - * * @param loginAttemptHandler login attempt handler * @param userDAO user dao * @param groupDAO group dao + * @param cacheManager + * @param groupResolver */ @Inject - public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO) { + public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO, CacheManager cacheManager, GroupResolver groupResolver) { this.loginAttemptHandler = loginAttemptHandler; this.userDAO = userDAO; - this.groupCollector = new GroupCollector(groupDAO); + this.groupResolver = groupResolver; + this.cacheManager = cacheManager; } /** @@ -69,7 +73,7 @@ public final class DAORealmHelperFactory { * @return new {@link DAORealmHelper} instance. */ public DAORealmHelper create(String realm) { - return new DAORealmHelper(loginAttemptHandler, userDAO, groupCollector, realm); + return new DAORealmHelper(loginAttemptHandler, userDAO, realm); } } diff --git a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java b/scm-core/src/main/java/sonia/scm/security/GroupCollector.java index 56687af7ef..06ac590a9a 100644 --- a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java +++ b/scm-core/src/main/java/sonia/scm/security/GroupCollector.java @@ -3,10 +3,14 @@ package sonia.scm.security; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; import sonia.scm.group.Group; import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupNames; +import java.util.Set; + /** * Collect groups for a certain principal. * Warning: The class is only for internal use and should never used directly. @@ -15,18 +19,41 @@ class GroupCollector { private static final Logger LOG = LoggerFactory.getLogger(GroupCollector.class); + /** Field description */ + public static final String CACHE_NAME = "sonia.cache.externalGroups"; + + /** Field description */ + private final Cache cache; + private Set groupResolvers; + private final GroupDAO groupDAO; - GroupCollector(GroupDAO groupDAO) { + GroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set groupResolvers) { this.groupDAO = groupDAO; + this.cache = cacheManager.getCache(CACHE_NAME); + this.groupResolvers = groupResolvers; } - GroupNames collect(String principal, Iterable groupNames) { + Iterable collect(String principal) { + + Set externalGroups = cache.get(principal); + + if (externalGroups == null) { + ImmutableSet.Builder newExternalGroups = ImmutableSet.builder(); + + for (GroupResolver groupResolver : groupResolvers) { + Iterable groups = groupResolver.resolveGroups(principal); + groups.forEach(newExternalGroups::add); + } + + cache.put(principal, newExternalGroups.build()); + } + ImmutableSet.Builder builder = ImmutableSet.builder(); builder.add(GroupNames.AUTHENTICATED); - for (String group : groupNames) { + for (String group : externalGroups) { builder.add(group); } diff --git a/scm-core/src/main/java/sonia/scm/security/GroupResolver.java b/scm-core/src/main/java/sonia/scm/security/GroupResolver.java new file mode 100644 index 0000000000..3845628913 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/GroupResolver.java @@ -0,0 +1,9 @@ +package sonia.scm.security; + +import sonia.scm.plugin.ExtensionPoint; + +@ExtensionPoint +public interface GroupResolver { + + Iterable resolveGroups(String principal); +} diff --git a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java index d421d33f45..b209184902 100644 --- a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.AlreadyExistsException; import sonia.scm.NotFoundException; +import sonia.scm.cache.CacheManager; import sonia.scm.group.ExternalGroupNames; import sonia.scm.group.Group; import sonia.scm.group.GroupDAO; @@ -65,7 +66,7 @@ public final class SyncingRealmHelper { private final AdministrationContext ctx; private final UserManager userManager; private final GroupManager groupManager; - private final GroupCollector groupCollector; + private final CacheManager cacheManager; /** * Constructs a new SyncingRealmHelper. @@ -76,11 +77,11 @@ public final class SyncingRealmHelper { * @param groupDAO group dao */ @Inject - public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, GroupDAO groupDAO) { + public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, GroupDAO groupDAO, CacheManager cacheManager) { this.ctx = ctx; this.userManager = userManager; this.groupManager = groupManager; - this.groupCollector = new GroupCollector(groupDAO); + this.cacheManager = cacheManager; } /** @@ -199,7 +200,6 @@ public final class SyncingRealmHelper { collection.add(user.getId(), realm); collection.add(user, realm); - collection.add(groupCollector.collect(user.getId(), groups), realm); collection.add(new ExternalGroupNames(externalGroups), realm); return new SimpleAuthenticationInfo(collection, user.getPassword()); diff --git a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java index 78dbd4fdd2..0fbcc20ac0 100644 --- a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java @@ -1,20 +1,16 @@ package sonia.scm.security; -import com.google.common.collect.ImmutableList; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.PrincipalCollection; -import org.junit.Ignore; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.group.Group; import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserDAO; @@ -38,7 +34,7 @@ class DAORealmHelperTest { @BeforeEach void setUpObjectUnderTest() { - helper = new DAORealmHelper(loginAttemptHandler, userDAO, new GroupCollector(groupDAO), "hitchhiker"); + helper = new DAORealmHelper(loginAttemptHandler, userDAO, "hitchhiker"); } @Test @@ -73,29 +69,9 @@ class DAORealmHelperTest { AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build(); PrincipalCollection principals = authenticationInfo.getPrincipals(); assertThat(principals.oneByType(User.class)).isSameAs(user); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated"); assertThat(principals.oneByType(Scope.class)).isEmpty(); } - @Test - @Ignore - void shouldReturnAuthenticationInfoWithGroups() { - User user = new User("trillian"); - when(userDAO.get("trillian")).thenReturn(user); - - Group one = new Group("xml", "one", "trillian"); - Group two = new Group("xml", "two", "trillian"); - Group six = new Group("xml", "six", "dent"); - when(groupDAO.getAll()).thenReturn(ImmutableList.of(one, two, six)); - - AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian") - .withGroups(ImmutableList.of("three")) - .build(); - - PrincipalCollection principals = authenticationInfo.getPrincipals(); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated", "one", "two", "three"); - } - @Test void shouldReturnAuthenticationInfoWithScope() { User user = new User("trillian"); @@ -148,7 +124,6 @@ class DAORealmHelperTest { PrincipalCollection principals = authenticationInfo.getPrincipals(); assertThat(principals.oneByType(User.class)).isSameAs(user); - assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated"); assertThat(principals.oneByType(Scope.class)).isEmpty(); assertThat(authenticationInfo.getCredentials()).isNull(); diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java index 20d1010b57..b7fbd97aac 100644 --- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java @@ -36,7 +36,6 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Throwables; -import com.google.common.collect.Lists; import org.apache.shiro.authc.AuthenticationInfo; import org.assertj.core.api.Assertions; import org.junit.Before; @@ -45,22 +44,26 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.AlreadyExistsException; +import sonia.scm.cache.CacheManager; import sonia.scm.group.ExternalGroupNames; import sonia.scm.group.Group; import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupManager; -import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.PrivilegedAction; import java.io.IOException; -import java.util.List; import static org.hamcrest.Matchers.hasItem; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ @@ -81,6 +84,9 @@ public class SyncingRealmHelperTest { @Mock private GroupDAO groupDAO; + @Mock + CacheManager cacheManager; + private SyncingRealmHelper helper; /** @@ -106,7 +112,7 @@ public class SyncingRealmHelperTest { } }; - helper = new SyncingRealmHelper(ctx, userManager, groupManager, groupDAO); + helper = new SyncingRealmHelper(ctx, userManager, groupManager, groupDAO, cacheManager); } /** @@ -183,19 +189,6 @@ public class SyncingRealmHelperTest { verify(userManager, times(1)).modify(user); } - @Test - public void builderShouldSetInternalGroups() { - AuthenticationInfo authenticationInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(new User("ziltoid")) - .withGroups("internal") - .build(); - - GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class); - Assertions.assertThat(groupNames.getCollection()).contains("_authenticated", "internal"); - } - @Test public void builderShouldSetExternalGroups() { AuthenticationInfo authenticationInfo = helper @@ -223,27 +216,4 @@ public class SyncingRealmHelperTest { assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test")); assertEquals(user, authInfo.getPrincipals().oneByType(User.class)); } - - @Test - public void shouldReturnCombinedGroupNames() { - User user = new User("tricia"); - - List groups = Lists.newArrayList(new Group("xml", "heartOfGold", "tricia")); - when(groupDAO.getAll()).thenReturn(groups); - - AuthenticationInfo authInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(user) - .withGroups("fjordsOfAfrican") - .withExternalGroups("g42") - .build(); - - - GroupNames groupNames = authInfo.getPrincipals().oneByType(GroupNames.class); - Assertions.assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican"); - - ExternalGroupNames externalGroupNames = authInfo.getPrincipals().oneByType(ExternalGroupNames.class); - Assertions.assertThat(externalGroupNames).contains("g42"); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java index b237a0a5ff..324dfe9082 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java @@ -104,7 +104,6 @@ public class BearerRealm extends AuthenticatingRealm return helper.authenticationInfoBuilder(accessToken.getSubject()) .withCredentials(bt.getCredentials()) .withScope(Scopes.fromClaims(accessToken.getClaims())) - .withGroups(accessToken.getGroups()) .build(); } diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java index c2d75358fd..d458f9c72c 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -90,12 +90,10 @@ class BearerRealmTest { Set groups = ImmutableSet.of("HeartOfGold", "Puzzle42"); when(accessToken.getSubject()).thenReturn("trillian"); - when(accessToken.getGroups()).thenReturn(groups); when(accessToken.getClaims()).thenReturn(new HashMap<>()); when(accessTokenResolver.resolve(bearerToken)).thenReturn(accessToken); when(realmHelper.authenticationInfoBuilder("trillian")).thenReturn(builder); - when(builder.withGroups(groups)).thenReturn(builder); when(builder.withCredentials("__bearer__")).thenReturn(builder); when(builder.withScope(any(Scope.class))).thenReturn(builder); when(builder.build()).thenReturn(authenticationInfo); From 2cff893d738da2cfde605af1d3eeb8934d2b758c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 1 Aug 2019 16:05:22 +0200 Subject: [PATCH 077/106] add interface in core + move groupCollector to webapp --- .../sonia/scm/security/GroupCollector.java | 69 +----------------- .../scm/group/DefaultGroupCollector.java | 70 +++++++++++++++++++ .../sonia/scm/group}/GroupCollectorTest.java | 14 ++-- 3 files changed, 77 insertions(+), 76 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java rename {scm-core/src/test/java/sonia/scm/security => scm-webapp/src/test/java/sonia/scm/group}/GroupCollectorTest.java (74%) diff --git a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java b/scm-core/src/main/java/sonia/scm/security/GroupCollector.java index 06ac590a9a..6c9bf2e659 100644 --- a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java +++ b/scm-core/src/main/java/sonia/scm/security/GroupCollector.java @@ -1,70 +1,5 @@ package sonia.scm.security; -import com.google.common.collect.ImmutableSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.cache.Cache; -import sonia.scm.cache.CacheManager; -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; - -import java.util.Set; - -/** - * Collect groups for a certain principal. - * Warning: The class is only for internal use and should never used directly. - */ -class GroupCollector { - - private static final Logger LOG = LoggerFactory.getLogger(GroupCollector.class); - - /** Field description */ - public static final String CACHE_NAME = "sonia.cache.externalGroups"; - - /** Field description */ - private final Cache cache; - private Set groupResolvers; - - private final GroupDAO groupDAO; - - GroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set groupResolvers) { - this.groupDAO = groupDAO; - this.cache = cacheManager.getCache(CACHE_NAME); - this.groupResolvers = groupResolvers; - } - - Iterable collect(String principal) { - - Set externalGroups = cache.get(principal); - - if (externalGroups == null) { - ImmutableSet.Builder newExternalGroups = ImmutableSet.builder(); - - for (GroupResolver groupResolver : groupResolvers) { - Iterable groups = groupResolver.resolveGroups(principal); - groups.forEach(newExternalGroups::add); - } - - cache.put(principal, newExternalGroups.build()); - } - - ImmutableSet.Builder builder = ImmutableSet.builder(); - - builder.add(GroupNames.AUTHENTICATED); - - for (String group : externalGroups) { - builder.add(group); - } - - for (Group group : groupDAO.getAll()) { - if (group.isMember(principal)) { - builder.add(group.getName()); - } - } - - GroupNames groups = new GroupNames(builder.build()); - LOG.debug("collected following groups for principal {}: {}", principal, groups); - return groups; - } +public interface GroupCollector { + Iterable collect(String principal); } diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java new file mode 100644 index 0000000000..cdaf0eb7b2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java @@ -0,0 +1,70 @@ +package sonia.scm.group; + +import com.google.common.collect.ImmutableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; +import sonia.scm.security.GroupCollector; +import sonia.scm.security.GroupResolver; + +import java.util.Set; + +/** + * Collect groups for a certain principal. + * Warning: The class is only for internal use and should never used directly. + */ +class DefaultGroupCollector implements GroupCollector { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultGroupCollector.class); + + /** Field description */ + public static final String CACHE_NAME = "sonia.cache.externalGroups"; + + /** Field description */ + private final Cache> cache; + private Set groupResolvers; + + private final GroupDAO groupDAO; + + DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set groupResolvers) { + this.groupDAO = groupDAO; + this.cache = cacheManager.getCache(CACHE_NAME); + this.groupResolvers = groupResolvers; + } + + @Override + public Iterable collect(String principal) { + + Set externalGroups = cache.get(principal); + + if (externalGroups == null) { + ImmutableSet.Builder newExternalGroups = ImmutableSet.builder(); + + for (GroupResolver groupResolver : groupResolvers) { + Iterable groups = groupResolver.resolveGroups(principal); + groups.forEach(newExternalGroups::add); + } + + cache.put(principal, newExternalGroups.build()); + } + + ImmutableSet.Builder builder = ImmutableSet.builder(); + + builder.add(GroupNames.AUTHENTICATED); + + for (String group : externalGroups) { + builder.add(group); + } + + for (Group group : groupDAO.getAll()) { + if (group.isMember(principal)) { + builder.add(group.getName()); + } + } + + GroupNames groups = new GroupNames(builder.build()); + LOG.debug("collected following groups for principal {}: {}", principal, groups); + return groups; + } +} diff --git a/scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/group/GroupCollectorTest.java similarity index 74% rename from scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java rename to scm-webapp/src/test/java/sonia/scm/group/GroupCollectorTest.java index 3fb59d1614..4bdcd2694a 100644 --- a/scm-core/src/test/java/sonia/scm/security/GroupCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/group/GroupCollectorTest.java @@ -1,6 +1,5 @@ -package sonia.scm.security; +package sonia.scm.group; -import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -9,11 +8,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; +import sonia.scm.security.GroupCollector; -import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -30,7 +26,7 @@ class GroupCollectorTest { @Test void shouldAlwaysReturnAuthenticatedGroup() { - GroupNames groupNames = collector.collect("trillian", Collections.emptySet()); + Iterable groupNames = collector.collect("trillian"); assertThat(groupNames).containsOnly("_authenticated"); } @@ -49,13 +45,13 @@ class GroupCollectorTest { @Test void shouldReturnGroupsFromDao() { - GroupNames groupNames = collector.collect("trillian", Collections.emptySet()); + Iterable groupNames = collector.collect("trillian"); assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican"); } @Test void shouldCombineGivenWithDao() { - GroupNames groupNames = collector.collect("trillian", ImmutableList.of("awesome", "incredible")); + Iterable groupNames = collector.collect("trillian"); assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible"); } From 8550baaea902b041481e3af251c1e86cac5230cc Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 08:17:17 +0200 Subject: [PATCH 078/106] refactor GroupResolver + GroupCollector --- .../java/sonia/scm/group/GroupCollector.java | 10 ++ .../{security => group}/GroupResolver.java | 7 +- .../sonia/scm/security/GroupCollector.java | 5 - .../scm/group/DefaultGroupCollector.java | 70 ++++++------ .../scm/group/DefaultGroupCollectorTest.java | 100 ++++++++++++++++++ .../sonia/scm/group/GroupCollectorTest.java | 60 ----------- 6 files changed, 150 insertions(+), 102 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/group/GroupCollector.java rename scm-core/src/main/java/sonia/scm/{security => group}/GroupResolver.java (51%) delete mode 100644 scm-core/src/main/java/sonia/scm/security/GroupCollector.java create mode 100644 scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java delete mode 100644 scm-webapp/src/test/java/sonia/scm/group/GroupCollectorTest.java diff --git a/scm-core/src/main/java/sonia/scm/group/GroupCollector.java b/scm-core/src/main/java/sonia/scm/group/GroupCollector.java new file mode 100644 index 0000000000..4546db1bc4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupCollector.java @@ -0,0 +1,10 @@ +package sonia.scm.group; + +import java.util.Set; + +public interface GroupCollector { + + String AUTHENTICATED = "_authenticated"; + + Set collect(String principal); +} diff --git a/scm-core/src/main/java/sonia/scm/security/GroupResolver.java b/scm-core/src/main/java/sonia/scm/group/GroupResolver.java similarity index 51% rename from scm-core/src/main/java/sonia/scm/security/GroupResolver.java rename to scm-core/src/main/java/sonia/scm/group/GroupResolver.java index 3845628913..5aba63c93b 100644 --- a/scm-core/src/main/java/sonia/scm/security/GroupResolver.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupResolver.java @@ -1,9 +1,10 @@ -package sonia.scm.security; +package sonia.scm.group; import sonia.scm.plugin.ExtensionPoint; +import java.util.Set; + @ExtensionPoint public interface GroupResolver { - - Iterable resolveGroups(String principal); + Set resolve(String principal); } diff --git a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java b/scm-core/src/main/java/sonia/scm/security/GroupCollector.java deleted file mode 100644 index 6c9bf2e659..0000000000 --- a/scm-core/src/main/java/sonia/scm/security/GroupCollector.java +++ /dev/null @@ -1,5 +0,0 @@ -package sonia.scm.security; - -public interface GroupCollector { - Iterable collect(String principal); -} diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java index cdaf0eb7b2..8c6bac8103 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java @@ -1,70 +1,72 @@ package sonia.scm.group; +import com.cronutils.utils.VisibleForTesting; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.security.GroupCollector; -import sonia.scm.security.GroupResolver; +import javax.inject.Inject; +import javax.inject.Singleton; import java.util.Set; /** * Collect groups for a certain principal. * Warning: The class is only for internal use and should never used directly. */ -class DefaultGroupCollector implements GroupCollector { +@Singleton +public class DefaultGroupCollector implements GroupCollector { private static final Logger LOG = LoggerFactory.getLogger(DefaultGroupCollector.class); - /** Field description */ - public static final String CACHE_NAME = "sonia.cache.externalGroups"; - - /** Field description */ - private final Cache> cache; - private Set groupResolvers; + @VisibleForTesting + static final String CACHE_NAME = "sonia.cache.externalGroups"; private final GroupDAO groupDAO; + private final Cache> cache; + private final Set groupResolvers; - DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set groupResolvers) { + @Inject + public DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set groupResolvers) { this.groupDAO = groupDAO; - this.cache = cacheManager.getCache(CACHE_NAME); + this.cache = cacheManager.getCache(CACHE_NAME); this.groupResolvers = groupResolvers; } @Override - public Iterable collect(String principal) { + public Set collect(String principal) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + builder.add(AUTHENTICATED); + builder.addAll(resolveExternalGroups(principal)); + appendInternalGroups(principal, builder); + + Set groups = builder.build(); + LOG.debug("collected following groups for principal {}: {}", principal, groups); + return groups; + } + + private void appendInternalGroups(String principal, ImmutableSet.Builder builder) { + for (Group group : groupDAO.getAll()) { + if (group.isMember(principal)) { + builder.add(group.getName()); + } + } + } + + private Set resolveExternalGroups(String principal) { Set externalGroups = cache.get(principal); if (externalGroups == null) { ImmutableSet.Builder newExternalGroups = ImmutableSet.builder(); for (GroupResolver groupResolver : groupResolvers) { - Iterable groups = groupResolver.resolveGroups(principal); - groups.forEach(newExternalGroups::add); + newExternalGroups.addAll(groupResolver.resolve(principal)); } - - cache.put(principal, newExternalGroups.build()); + externalGroups = newExternalGroups.build(); + cache.put(principal, externalGroups); } - - ImmutableSet.Builder builder = ImmutableSet.builder(); - - builder.add(GroupNames.AUTHENTICATED); - - for (String group : externalGroups) { - builder.add(group); - } - - for (Group group : groupDAO.getAll()) { - if (group.isMember(principal)) { - builder.add(group.getName()); - } - } - - GroupNames groups = new GroupNames(builder.build()); - LOG.debug("collected following groups for principal {}: {}", principal, groups); - return groups; + return externalGroups; } } diff --git a/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java new file mode 100644 index 0000000000..edd23151b9 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/group/DefaultGroupCollectorTest.java @@ -0,0 +1,100 @@ +package sonia.scm.group; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cache.MapCache; +import sonia.scm.cache.MapCacheManager; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultGroupCollectorTest { + + @Mock + private GroupDAO groupDAO; + + @Mock + private GroupResolver groupResolver; + + private MapCacheManager mapCacheManager; + + private Set groupResolvers; + + private DefaultGroupCollector collector; + + @BeforeEach + void initCollector() { + groupResolvers = new HashSet<>(); + mapCacheManager = new MapCacheManager(); + collector = new DefaultGroupCollector(groupDAO, mapCacheManager, groupResolvers); + } + + @Test + void shouldAlwaysReturnAuthenticatedGroup() { + Iterable groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated"); + } + + @Test + void shouldReturnGroupsFromCache() { + MapCache> cache = mapCacheManager.getCache(DefaultGroupCollector.CACHE_NAME); + cache.put("trillian", ImmutableSet.of("awesome", "incredible")); + + Set groups = collector.collect("trillian"); + assertThat(groups).containsOnly("_authenticated", "awesome", "incredible"); + } + + @Test + void shouldNotCallResolverIfExternalGroupsAreCached() { + groupResolvers.add(groupResolver); + + MapCache> cache = mapCacheManager.getCache(DefaultGroupCollector.CACHE_NAME); + cache.put("trillian", ImmutableSet.of("awesome", "incredible")); + + Set groups = collector.collect("trillian"); + assertThat(groups).containsOnly("_authenticated", "awesome", "incredible"); + + verify(groupResolver, never()).resolve("trillian"); + } + + @Nested + class WithGroupsFromDao { + + @BeforeEach + void setUpGroupsDao() { + List groups = Lists.newArrayList( + new Group("xml", "heartOfGold", "trillian"), + new Group("xml", "g42", "dent", "prefect"), + new Group("xml", "fjordsOfAfrican", "dent", "trillian") + ); + when(groupDAO.getAll()).thenReturn(groups); + } + + @Test + void shouldReturnGroupsFromDao() { + Iterable groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican"); + } + + @Test + void shouldCombineWithResolvers() { + when(groupResolver.resolve("trillian")).thenReturn(ImmutableSet.of("awesome", "incredible")); + groupResolvers.add(groupResolver); + Iterable groupNames = collector.collect("trillian"); + assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible"); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/group/GroupCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/group/GroupCollectorTest.java deleted file mode 100644 index 4bdcd2694a..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/group/GroupCollectorTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package sonia.scm.group; - -import com.google.common.collect.Lists; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.security.GroupCollector; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class GroupCollectorTest { - - @Mock - private GroupDAO groupDAO; - - @InjectMocks - private GroupCollector collector; - - @Test - void shouldAlwaysReturnAuthenticatedGroup() { - Iterable groupNames = collector.collect("trillian"); - assertThat(groupNames).containsOnly("_authenticated"); - } - - @Nested - class WithGroupsFromDao { - - @BeforeEach - void setUpGroupsDao() { - List groups = Lists.newArrayList( - new Group("xml", "heartOfGold", "trillian"), - new Group("xml", "g42", "dent", "prefect"), - new Group("xml", "fjordsOfAfrican", "dent", "trillian") - ); - when(groupDAO.getAll()).thenReturn(groups); - } - - @Test - void shouldReturnGroupsFromDao() { - Iterable groupNames = collector.collect("trillian"); - assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican"); - } - - @Test - void shouldCombineGivenWithDao() { - Iterable groupNames = collector.collect("trillian"); - assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible"); - } - - } - -} From 442aacbcdb073baa7fbcba60b5b34256bec11ffb Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 09:32:44 +0200 Subject: [PATCH 079/106] remove GroupNames and ExternalGroupNames in favor of GroupCollector --- .../sonia/scm/group/ExternalGroupNames.java | 22 --- .../main/java/sonia/scm/group/GroupNames.java | 187 ------------------ .../scm/security/AccessTokenBuilder.java | 9 - .../scm/security/DAORealmHelperFactory.java | 7 +- .../scm/security/SyncingRealmHelper.java | 122 +----------- .../scm/security/SyncingRealmHelperTest.java | 30 +-- .../sonia/scm/api/v2/resources/MeDto.java | 4 +- .../scm/api/v2/resources/MeDtoFactory.java | 18 +- .../DefaultAuthorizationCollector.java | 32 +-- .../java/sonia/scm/security/DefaultRealm.java | 18 +- .../scm/security/JwtAccessTokenBuilder.java | 18 -- .../DefaultAdministrationContext.java | 10 +- .../api/v2/resources/MeDtoFactoryTest.java | 26 +-- .../DefaultAuthorizationCollectorTest.java | 16 +- .../sonia/scm/security/DefaultRealmTest.java | 97 +++------ .../security/JwtAccessTokenBuilderTest.java | 50 +---- 16 files changed, 100 insertions(+), 566 deletions(-) delete mode 100644 scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java delete mode 100644 scm-core/src/main/java/sonia/scm/group/GroupNames.java diff --git a/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java b/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java deleted file mode 100644 index 179e488236..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/ExternalGroupNames.java +++ /dev/null @@ -1,22 +0,0 @@ -package sonia.scm.group; - -import java.util.Collection; - -/** - * This class represents all associated groups which are provided by external systems for a certain user. - * - * @author Sebastian Sdorra - * @since 2.0.0 - */ -public class ExternalGroupNames extends GroupNames { - public ExternalGroupNames() { - } - - public ExternalGroupNames(String groupName, String... groupNames) { - super(groupName, groupNames); - } - - public ExternalGroupNames(Collection collection) { - super(collection); - } -} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupNames.java b/scm-core/src/main/java/sonia/scm/group/GroupNames.java deleted file mode 100644 index c28f9f5ef1..0000000000 --- a/scm-core/src/main/java/sonia/scm/group/GroupNames.java +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.group; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Joiner; -import com.google.common.base.Objects; -import com.google.common.collect.Lists; - -import java.io.Serializable; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * This class represents all associated groups for a user. - * - * @author Sebastian Sdorra - * @since 1.21 - */ -public class GroupNames implements Serializable, Iterable -{ - - /** - * Group for all authenticated users - * @since 1.31 - */ - public static final String AUTHENTICATED = "_authenticated"; - - /** Field description */ - private static final long serialVersionUID = 8615685985213897947L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public GroupNames() - { - this(Collections.emptyList()); - } - - /** - * Constructs ... - * - * - * @param groupName - * @param groupNames - */ - public GroupNames(String groupName, String... groupNames) - { - this(Lists.asList(groupName, groupNames)); - } - - /** - * Constructs ... - * - * - * @param collection - */ - public GroupNames(Collection collection) - { - this.collection = Collections.unmodifiableCollection(collection); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param groupName - * - * @return - */ - public boolean contains(String groupName) - { - return collection.contains(groupName); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final GroupNames other = (GroupNames) obj; - - return Objects.equal(collection, other.collection); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() - { - return Objects.hashCode(collection); - } - - /** - * Method description - * - * - * @return - */ - @Override - public Iterator iterator() - { - return collection.iterator(); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - return Joiner.on(", ").join(collection); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Collection getCollection() - { - return collection; - } - - - //~--- fields --------------------------------------------------------------- - /** Field description */ - private final Collection collection; -} diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java index 0924716bd8..afe81ac27f 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java @@ -99,15 +99,6 @@ public interface AccessTokenBuilder { */ AccessTokenBuilder scope(Scope scope); - /** - * Define the logged in user as member of the given groups. - * - * @param groups group names - * - * @return {@code this} - */ - AccessTokenBuilder groups(String... groups); - /** * Creates a new {@link AccessToken} with the provided settings. * diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java index b503ff8375..dd59de2ac8 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelperFactory.java @@ -31,7 +31,6 @@ package sonia.scm.security; import sonia.scm.cache.CacheManager; -import sonia.scm.group.GroupDAO; import sonia.scm.user.UserDAO; import javax.inject.Inject; @@ -47,21 +46,17 @@ public final class DAORealmHelperFactory { private final LoginAttemptHandler loginAttemptHandler; private final UserDAO userDAO; private final CacheManager cacheManager; - private final GroupResolver groupResolver; /** * Constructs a new instance. * @param loginAttemptHandler login attempt handler * @param userDAO user dao - * @param groupDAO group dao * @param cacheManager - * @param groupResolver */ @Inject - public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO, CacheManager cacheManager, GroupResolver groupResolver) { + public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, CacheManager cacheManager) { this.loginAttemptHandler = loginAttemptHandler; this.userDAO = userDAO; - this.groupResolver = groupResolver; this.cacheManager = cacheManager; } diff --git a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java index b209184902..b2175f304a 100644 --- a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java @@ -32,25 +32,15 @@ import com.google.inject.Inject; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.subject.SimplePrincipalCollection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import sonia.scm.AlreadyExistsException; import sonia.scm.NotFoundException; -import sonia.scm.cache.CacheManager; -import sonia.scm.group.ExternalGroupNames; import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupManager; import sonia.scm.plugin.Extension; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; -import java.util.Collection; -import java.util.Collections; - -import static java.util.Arrays.asList; - /** * Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated * users with the local database. @@ -61,12 +51,9 @@ import static java.util.Arrays.asList; @Extension public final class SyncingRealmHelper { - private static final Logger LOG = LoggerFactory.getLogger(SyncingRealmHelper.class); - private final AdministrationContext ctx; private final UserManager userManager; private final GroupManager groupManager; - private final CacheManager cacheManager; /** * Constructs a new SyncingRealmHelper. @@ -74,133 +61,28 @@ public final class SyncingRealmHelper { * @param ctx administration context * @param userManager user manager * @param groupManager group manager - * @param groupDAO group dao */ @Inject - public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, GroupDAO groupDAO, CacheManager cacheManager) { + public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager) { this.ctx = ctx; this.userManager = userManager; this.groupManager = groupManager; - this.cacheManager = cacheManager; } - /** - * Create {@link AuthenticationInfo} from user and groups. - */ - public AuthenticationInfoBuilder.ForRealm authenticationInfo() { - return new AuthenticationInfoBuilder().new ForRealm(); - } - - public class AuthenticationInfoBuilder { - private String realm; - private User user; - private Collection groups = Collections.emptySet(); - private Collection externalGroups = Collections.emptySet(); - - private AuthenticationInfo build() { - return SyncingRealmHelper.this.createAuthenticationInfo(realm, user, groups, externalGroups); - } - - public class ForRealm { - private ForRealm() { - } - - /** - * Sets the realm. - * @param realm name of the realm - */ - public ForUser forRealm(String realm) { - AuthenticationInfoBuilder.this.realm = realm; - return AuthenticationInfoBuilder.this.new ForUser(); - } - } - - public class ForUser { - private ForUser() { - } - - /** - * Sets the user. - * @param user authenticated user - */ - public AuthenticationInfoBuilder.WithGroups andUser(User user) { - AuthenticationInfoBuilder.this.user = user; - return AuthenticationInfoBuilder.this.new WithGroups(); - } - } - - public class WithGroups { - private WithGroups() { - } - - /** - * Set the internal groups for the user. - * @param groups groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withGroups(String... groups) { - return withGroups(asList(groups)); - } - - /** - * Set the internal groups for the user. - * @param groups groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withGroups(Collection groups) { - AuthenticationInfoBuilder.this.groups = groups; - return this; - } - - /** - * Set the external groups for the user. - * @param externalGroups external groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withExternalGroups(String... externalGroups) { - return withExternalGroups(asList(externalGroups)); - } - - /** - * Set the external groups for the user. - * @param externalGroups external groups of the authenticated user - * @return builder step for groups - */ - public WithGroups withExternalGroups(Collection externalGroups) { - AuthenticationInfoBuilder.this.externalGroups = externalGroups; - return this; - } - - /** - * Builds the {@link AuthenticationInfo} from the given options. - * - * @return complete autentication info - */ - public AuthenticationInfo build() { - return AuthenticationInfoBuilder.this.build(); - } - } - } - - //~--- methods -------------------------------------------------------------- - /** * Create {@link AuthenticationInfo} from user and groups. * * * @param realm name of the realm * @param user authenticated user - * @param groups groups of the authenticated user * * @return authentication info */ - private AuthenticationInfo createAuthenticationInfo(String realm, User user, - Collection groups, Collection externalGroups) { + public AuthenticationInfo createAuthenticationInfo(String realm, User user) { SimplePrincipalCollection collection = new SimplePrincipalCollection(); collection.add(user.getId(), realm); collection.add(user, realm); - collection.add(new ExternalGroupNames(externalGroups), realm); return new SimpleAuthenticationInfo(collection, user.getPassword()); } diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java index b7fbd97aac..ca7c2efdc6 100644 --- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java @@ -37,17 +37,13 @@ package sonia.scm.security; import com.google.common.base.Throwables; import org.apache.shiro.authc.AuthenticationInfo; -import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.AlreadyExistsException; -import sonia.scm.cache.CacheManager; -import sonia.scm.group.ExternalGroupNames; import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupManager; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -81,12 +77,6 @@ public class SyncingRealmHelperTest { @Mock private UserManager userManager; - @Mock - private GroupDAO groupDAO; - - @Mock - CacheManager cacheManager; - private SyncingRealmHelper helper; /** @@ -112,7 +102,7 @@ public class SyncingRealmHelperTest { } }; - helper = new SyncingRealmHelper(ctx, userManager, groupManager, groupDAO, cacheManager); + helper = new SyncingRealmHelper(ctx, userManager, groupManager); } /** @@ -189,27 +179,11 @@ public class SyncingRealmHelperTest { verify(userManager, times(1)).modify(user); } - @Test - public void builderShouldSetExternalGroups() { - AuthenticationInfo authenticationInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(new User("ziltoid")) - .withExternalGroups("external") - .build(); - - ExternalGroupNames groupNames = authenticationInfo.getPrincipals().oneByType(ExternalGroupNames.class); - Assertions.assertThat(groupNames.getCollection()).containsOnly("external"); - } @Test public void builderShouldSetValues() { User user = new User("ziltoid"); - AuthenticationInfo authInfo = helper - .authenticationInfo() - .forRealm("unit-test") - .andUser(user) - .build(); + AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test", user); assertNotNull(authInfo); assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java index 84fbbfe290..a87ad7df17 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java @@ -7,7 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.List; +import java.util.Set; @Getter @Setter @@ -17,7 +17,7 @@ public class MeDto extends HalRepresentation { private String name; private String displayName; private String mail; - private List groups; + private Set groups; MeDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java index b5e1998066..c2bebd389a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java @@ -1,18 +1,16 @@ package sonia.scm.api.v2.resources; -import com.google.common.collect.ImmutableList; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserPermissions; import javax.inject.Inject; -import java.util.Collections; import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; @@ -22,11 +20,13 @@ public class MeDtoFactory extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final UserManager userManager; + private final GroupCollector groupCollector; @Inject - public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager) { + public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector) { this.resourceLinks = resourceLinks; this.userManager = userManager; + this.groupCollector = groupCollector; } public MeDto create() { @@ -35,16 +35,12 @@ public class MeDtoFactory extends HalAppenderMapper { MeDto dto = createDto(user); mapUserProperties(user, dto); - mapGroups(principals, dto); + mapGroups(user, dto); return dto; } - private void mapGroups(PrincipalCollection principals, MeDto dto) { - Iterable groups = principals.oneByType(GroupNames.class); - if (groups == null) { - groups = Collections.emptySet(); - } - dto.setGroups(ImmutableList.copyOf(groups)); + private void mapGroups(User user, MeDto dto) { + dto.setGroups(groupCollector.collect(user.getName())); } private void mapUserProperties(User user, MeDto dto) { diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index baff3b951c..28f61df34f 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -52,17 +52,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.Extension; -import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.user.User; import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; import java.util.Collection; +import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -88,19 +89,21 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector /** * Constructs ... - * @param cacheManager + * @param cacheManager * @param repositoryDAO * @param securitySystem * @param repositoryPermissionProvider + * @param groupCollector */ @Inject public DefaultAuthorizationCollector(CacheManager cacheManager, - RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider) + RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider, GroupCollector groupCollector) { this.cache = cacheManager.getCache(CACHE_NAME); this.repositoryDAO = repositoryDAO; this.securitySystem = securitySystem; this.repositoryPermissionProvider = repositoryPermissionProvider; + this.groupCollector = groupCollector; } //~--- methods -------------------------------------------------------------- @@ -145,16 +148,16 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector Preconditions.checkNotNull(user, "no user found in principal collection"); - GroupNames groupNames = principals.oneByType(GroupNames.class); + Set groups = groupCollector.collect(user.getName()); - CacheKey cacheKey = new CacheKey(user.getId(), groupNames); + CacheKey cacheKey = new CacheKey(user.getId(), groups); AuthorizationInfo info = cache.get(cacheKey); if (info == null) { logger.trace("collect AuthorizationInfo for user {}", user.getName()); - info = createAuthorizationInfo(user, groupNames); + info = createAuthorizationInfo(user, groups); cache.put(cacheKey, info); } else if (logger.isTraceEnabled()) @@ -166,7 +169,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectGlobalPermissions(Builder builder, - final User user, final GroupNames groups) + final User user, final Set groups) { Collection globalPermissions = securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input)); @@ -181,7 +184,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectRepositoryPermissions(Builder builder, User user, - GroupNames groups) + Set groups) { for (Repository repository : repositoryDAO.getAll()) { @@ -190,7 +193,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector } private void collectRepositoryPermissions(Builder builder, - Repository repository, User user, GroupNames groups) + Repository repository, User user, Set groups) { Collection repositoryPermissions = repository.getPermissions(); @@ -245,7 +248,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector .getVerbs(); } - private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) { + private AuthorizationInfo createAuthorizationInfo(User user, Set groups) { Builder builder = ImmutableSet.builder(); collectGlobalPermissions(builder, user, groups); @@ -279,7 +282,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector //~--- get methods ---------------------------------------------------------- - private boolean isUserPermitted(User user, GroupNames groups, + private boolean isUserPermitted(User user, Set groups, PermissionObject perm) { //J- @@ -314,7 +317,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector */ private static class CacheKey { - private CacheKey(String username, GroupNames groupnames) + private CacheKey(String username, Set groupnames) { this.username = username; this.groupnames = groupnames; @@ -356,7 +359,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector //~--- fields ------------------------------------------------------------- /** group names */ - private final GroupNames groupnames; + private final Set groupnames; /** username */ private final String username; @@ -374,4 +377,5 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private final SecuritySystem securitySystem; private final RepositoryPermissionProvider repositoryPermissionProvider; + private final GroupCollector groupCollector; } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java index bacbd9b314..245dcadb78 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java @@ -34,7 +34,6 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.annotations.VisibleForTesting; - import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; @@ -45,21 +44,16 @@ import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; - -import org.apache.shiro.subject.SimplePrincipalCollection; -import sonia.scm.group.GroupNames; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.plugin.Extension; -//~--- JDK imports ------------------------------------------------------------ - import javax.inject.Inject; import javax.inject.Singleton; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * Default authorizing realm. * @@ -149,7 +143,7 @@ public class DefaultRealm extends AuthorizingRealm LOG.trace("principal does not contain scope information, returning all permissions"); log(principals, info, null); } - + return info; } @@ -180,8 +174,6 @@ public class DefaultRealm extends AuthorizingRealm StringBuilder buffer = new StringBuilder("authorization summary: "); buffer.append(SEPARATOR).append("username : ").append(collection.getPrimaryPrincipal()); - buffer.append(SEPARATOR).append("groups : "); - append(buffer, collection.oneByType(GroupNames.class)); buffer.append(SEPARATOR).append("roles : "); append(buffer, original.getRoles()); buffer.append(SEPARATOR).append("scope : "); diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java index cc1d4be7a7..5a74f77502 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java @@ -40,11 +40,9 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.group.ExternalGroupNames; import java.time.Clock; import java.time.Instant; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -139,12 +137,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { return this; } - @Override - public JwtAccessTokenBuilder groups(String... groups) { - Collections.addAll(this.groups, groups); - return this; - } - JwtAccessTokenBuilder refreshExpiration(Instant refreshExpiration) { this.refreshExpiration = refreshExpiration; this.refreshableFor = 0; @@ -206,16 +198,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { claims.setIssuer(issuer); } - if (!groups.isEmpty()) { - claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups); - } else { - Subject currentSubject = SecurityUtils.getSubject(); - ExternalGroupNames externalGroupNames = currentSubject.getPrincipals().oneByType(ExternalGroupNames.class); - if (externalGroupNames != null) { - claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, externalGroupNames.getCollection().toArray(new String[]{})); - } - } - // sign token and create compact version String compact = Jwts.builder() .setClaims(claims) diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java b/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java index 0b380c8088..b62b6c63f3 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java +++ b/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java @@ -38,7 +38,6 @@ package sonia.scm.web.security; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; - import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; @@ -46,21 +45,17 @@ import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContext; -import sonia.scm.group.GroupNames; import sonia.scm.security.Role; import sonia.scm.user.User; import sonia.scm.util.AssertUtil; -//~--- JDK imports ------------------------------------------------------------ - +import javax.xml.bind.JAXB; import java.net.URL; -import javax.xml.bind.JAXB; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -161,7 +156,6 @@ public class DefaultAdministrationContext implements AdministrationContext collection.add(adminUser.getId(), REALM); collection.add(adminUser, REALM); - collection.add(new GroupNames(), REALM); collection.add(AdministrationContextMarker.MARKER, REALM); return collection; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java index 8a00c69229..d9572dc04c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java @@ -1,9 +1,9 @@ package sonia.scm.api.v2.resources; +import com.google.common.collect.ImmutableSet; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; -import org.assertj.core.util.Lists; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,7 +12,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserTestData; @@ -20,7 +20,6 @@ import sonia.scm.user.UserTestData; import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,6 +32,9 @@ class MeDtoFactoryTest { @Mock private UserManager userManager; + @Mock + private GroupCollector groupCollector; + @Mock private Subject subject; @@ -42,7 +44,7 @@ class MeDtoFactoryTest { void setUpContext() { ThreadContext.bind(subject); ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - meDtoFactory = new MeDtoFactory(resourceLinks, userManager); + meDtoFactory = new MeDtoFactory(resourceLinks, userManager, groupCollector); } @AfterEach @@ -69,24 +71,16 @@ class MeDtoFactoryTest { @Test void shouldCreateMeDtoWithGroups() { - prepareSubject(UserTestData.createTrillian(), "HeartOfGold", "Puzzle42"); + when(groupCollector.collect("trillian")).thenReturn(ImmutableSet.of("HeartOfGold", "Puzzle42")); + prepareSubject(UserTestData.createTrillian()); MeDto dto = meDtoFactory.create(); assertThat(dto.getGroups()).containsOnly("HeartOfGold", "Puzzle42"); } - private void prepareSubject(User user, String... groups) { + private void prepareSubject(User user) { PrincipalCollection collection = mock(PrincipalCollection.class); when(subject.getPrincipals()).thenReturn(collection); - when(collection.oneByType(any(Class.class))).then(ic -> { - Class type = ic.getArgument(0); - if (type.isAssignableFrom(User.class)) { - return user; - } else if (type.isAssignableFrom(GroupNames.class)) { - return new GroupNames(Lists.newArrayList(groups)); - } else { - return null; - } - }); + when(collection.oneByType(User.class)).thenReturn(user); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 8e7cb8a70e..930a06d249 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -33,6 +33,7 @@ package sonia.scm.security; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; @@ -49,7 +50,7 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; -import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupCollector; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryPermission; @@ -58,8 +59,6 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; -import java.util.Collections; - import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -96,6 +95,9 @@ public class DefaultAuthorizationCollectorTest { @Mock private RepositoryPermissionProvider repositoryPermissionProvider; + @Mock + private GroupCollector groupCollector; + private DefaultAuthorizationCollector collector; @Rule @@ -107,7 +109,7 @@ public class DefaultAuthorizationCollectorTest { @Before public void setUp(){ when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache); - collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider); + collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider, groupCollector); } /** @@ -290,9 +292,13 @@ public class DefaultAuthorizationCollectorTest { SimplePrincipalCollection spc = new SimplePrincipalCollection(); spc.add(user.getName(), "unit"); spc.add(user, "unit"); - spc.add(new GroupNames(group, groups), "unit"); Subject subject = new Subject.Builder().authenticated(true).principals(spc).buildSubject(); shiro.setSubject(subject); + + ImmutableSet.Builder builder = ImmutableSet.builder(); + builder.add(group); + builder.add(groups); + when(groupCollector.collect(user.getName())).thenReturn(builder.build()); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java index b6fea9e897..ba23411b36 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultRealmTest.java @@ -36,8 +36,6 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; - import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.DisabledAccountException; @@ -45,43 +43,44 @@ import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.DefaultPasswordService; -import org.apache.shiro.crypto.hash.DefaultHashService; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.SimplePrincipalCollection; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import sonia.scm.group.Group; -import sonia.scm.group.GroupDAO; -import sonia.scm.group.GroupNames; -import sonia.scm.user.User; -import sonia.scm.user.UserDAO; -import sonia.scm.user.UserTestData; - -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.authz.permission.WildcardPermissionResolver; +import org.apache.shiro.crypto.hash.DefaultHashService; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.SimplePrincipalCollection; import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.group.GroupDAO; +import sonia.scm.user.User; +import sonia.scm.user.UserDAO; +import sonia.scm.user.UserTestData; + +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -206,32 +205,6 @@ public class DefaultRealmTest ); } - /** - * Method description - * - */ - @Test - public void testGroupCollection() - { - User user = UserTestData.createTrillian(); - //J- - List groups = Lists.newArrayList( - new Group(DefaultRealm.REALM, "scm", user.getName()), - new Group(DefaultRealm.REALM, "developers", "perfect") - ); - //J+ - - when(groupDAO.getAll()).thenReturn(groups); - - UsernamePasswordToken token = daoUser(user, "secret"); - AuthenticationInfo info = realm.getAuthenticationInfo(token); - GroupNames groupNames = info.getPrincipals().oneByType(GroupNames.class); - - assertNotNull(groupNames); - assertThat(groupNames.getCollection(), hasSize(2)); - assertThat(groupNames, hasItems("scm", GroupNames.AUTHENTICATED)); - } - /** * Method description * @@ -251,12 +224,6 @@ public class DefaultRealmTest assertThat(collection.getRealmNames(), hasSize(1)); assertThat(collection.getRealmNames(), hasItem(DefaultRealm.REALM)); assertEquals(user, collection.oneByType(User.class)); - - GroupNames groups = collection.oneByType(GroupNames.class); - - assertNotNull(groups); - assertThat(groups.getCollection(), hasSize(1)); - assertThat(groups.getCollection(), hasItem(GroupNames.AUTHENTICATED)); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java index e2117235fc..7a8c0ef169 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java @@ -36,27 +36,25 @@ import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.Sets; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.group.ExternalGroupNames; -import java.util.Arrays; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; /** @@ -137,7 +135,6 @@ public class JwtAccessTokenBuilderTest { .issuer("https://www.scm-manager.org") .expiresIn(5, TimeUnit.SECONDS) .custom("a", "b") - .groups("one", "two", "three") .scope(Scope.valueOf("repo:*")) .build(); @@ -154,36 +151,6 @@ public class JwtAccessTokenBuilderTest { assertClaims(new JwtAccessToken(claims, compact)); } - @Test - public void testWithExternalGroups() { - applyExternalGroupsToSubject(true, "external"); - JwtAccessToken token = factory.create().subject("dent").build(); - assertArrayEquals(new String[]{"external"}, token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).map(x -> (String[]) x).get()); - } - - @Test - public void testWithInternalGroups() { - applyExternalGroupsToSubject(false, "external"); - JwtAccessToken token = factory.create().subject("dent").build(); - assertFalse(token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).isPresent()); - } - - private void applyExternalGroupsToSubject(boolean external, String... groups) { - Subject subject = spy(SecurityUtils.getSubject()); - when(subject.getPrincipals()).thenAnswer(invocation -> enrichWithGroups(invocation, groups, external)); - shiro.setSubject(subject); - } - - private Object enrichWithGroups(InvocationOnMock invocation, String[] groups, boolean external) throws Throwable { - PrincipalCollection principals = (PrincipalCollection) spy(invocation.callRealMethod()); - - List groupCollection = Arrays.asList(groups); - if (external) { - when(principals.oneByType(ExternalGroupNames.class)).thenReturn(new ExternalGroupNames(groupCollection)); - } - return principals; - } - private void assertClaims(JwtAccessToken token){ assertThat(token.getId(), not(isEmptyOrNullString())); assertNotNull( token.getIssuedAt() ); @@ -194,6 +161,5 @@ public class JwtAccessTokenBuilderTest { assertEquals(token.getIssuer().get(), "https://www.scm-manager.org"); assertEquals("b", token.getCustom("a").get()); assertEquals("[\"repo:*\"]", token.getScope().toString()); - assertThat(token.getGroups(), containsInAnyOrder("one", "two", "three")); } } From b029b80f63e53257d640f0ea17390b983ccd9477 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 09:54:40 +0200 Subject: [PATCH 080/106] fix failed unit test after refactoring MeDtoFactory --- .../sonia/scm/api/v2/resources/MeResourceTest.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index cd2a172c1b..7a3d1b4304 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableSet; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.credential.PasswordService; import org.apache.shiro.subject.PrincipalCollection; @@ -16,6 +17,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.ContextEntry; +import sonia.scm.group.GroupCollector; import sonia.scm.user.InvalidPasswordException; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -28,7 +30,12 @@ import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @@ -50,6 +57,9 @@ public class MeResourceTest { @Mock private ScmPathInfoStore scmPathInfoStore; + @Mock + private GroupCollector groupCollector; + @Mock private UserManager userManager; @@ -69,6 +79,7 @@ public class MeResourceTest { when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); + when(groupCollector.collect("trillian")).thenReturn(ImmutableSet.of("group1", "group2")); when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod(); when(userManager.getDefaultType()).thenReturn("xml"); MeResource meResource = new MeResource(meDtoFactory, userManager, passwordService); From ef0c57b83f1575b7f4a306483a96f6be06d6f40c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 09:55:30 +0200 Subject: [PATCH 081/106] bind GroupCollector to DefaultGroupCollector --- .../java/sonia/scm/lifecycle/modules/ScmServletModule.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java index 7ece64f719..cd34d03b50 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java @@ -50,8 +50,10 @@ import sonia.scm.cache.CacheManager; import sonia.scm.cache.GuavaCacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; +import sonia.scm.group.DefaultGroupCollector; import sonia.scm.group.DefaultGroupDisplayManager; import sonia.scm.group.DefaultGroupManager; +import sonia.scm.group.GroupCollector; import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupDisplayManager; import sonia.scm.group.GroupManager; @@ -195,7 +197,7 @@ class ScmServletModule extends ServletModule { bindDecorated(GroupManager.class, DefaultGroupManager.class, GroupManagerProvider.class); bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class); - + bind(GroupCollector.class, DefaultGroupCollector.class); bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class); // bind sslcontext provider From b4bee0758ddbafbe2c8d3ecc23383776aae88602 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 09:55:51 +0200 Subject: [PATCH 082/106] adjust caching for external groups --- scm-webapp/src/main/resources/config/gcache.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scm-webapp/src/main/resources/config/gcache.xml b/scm-webapp/src/main/resources/config/gcache.xml index eb59f36446..28876558df 100644 --- a/scm-webapp/src/main/resources/config/gcache.xml +++ b/scm-webapp/src/main/resources/config/gcache.xml @@ -40,15 +40,15 @@ /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 47e323e468daf1783d7c6b144b8cc03c8ad706ae Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 12:01:53 +0200 Subject: [PATCH 084/106] disable shiro cache for the DefaultRealm --- scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java index 245dcadb78..e65c88e679 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultRealm.java @@ -97,6 +97,9 @@ public class DefaultRealm extends AuthorizingRealm matcher.setPasswordService(service); setCredentialsMatcher(helper.wrapCredentialsMatcher(matcher)); setAuthenticationTokenClass(UsernamePasswordToken.class); + + // we cache in the AuthorizationCollector + setCachingEnabled(false); } //~--- methods -------------------------------------------------------------- From 0890707b3fe52e0b3ad344726176e9beb00d4b77 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 14:46:33 +0200 Subject: [PATCH 085/106] fix permission reference on clone repositories --- .../java/sonia/scm/repository/Repository.java | 4 +++- .../sonia/scm/repository/RepositoryTest.java | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 5bb50db06f..8c7000c25a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -82,7 +82,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per private String namespace; private String name; @XmlElement(name = "permission") - private final Set permissions = new HashSet<>(); + private Set permissions = new HashSet<>(); @XmlElement(name = "public") private boolean publicReadable = false; private boolean archived = false; @@ -331,6 +331,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per try { repository = (Repository) super.clone(); + // fix permission reference on clone + repository.permissions = new HashSet<>(permissions); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java new file mode 100644 index 0000000000..6053e10ad5 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java @@ -0,0 +1,22 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +class RepositoryTest { + + @Test + void shouldCreateNewPermissionOnClone() { + Repository repository = new Repository(); + repository.setPermissions(Arrays.asList(new RepositoryPermission("one", "role", false))); + + Repository cloned = repository.clone(); + cloned.setPermissions(Arrays.asList(new RepositoryPermission("two", "role", false))); + + assertThat(repository.getPermissions()).extracting(r -> r.getName()).containsOnly("one"); + } + +} From e33b4dc9fc16a1eb8eb30db48437726119b62782 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 14:46:58 +0200 Subject: [PATCH 086/106] removed unused method --- .../test/java/sonia/scm/it/utils/TestData.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index 20de47ffa4..d632f13f60 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -123,23 +123,6 @@ public class TestData { ; } - public static void createUserPermission(String username, String roleName, String repositoryType) { - String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); - LOG.info("create permission with name {} and role {} using the endpoint: {}", username, roleName, defaultPermissionUrl); - given(VndMediaType.REPOSITORY_PERMISSION) - .when() - .content("{\n" + - "\t\"role\": " + roleName + ",\n" + - "\t\"name\": \"" + username + "\",\n" + - "\t\"groupPermission\": false\n" + - "\t\n" + - "}") - .post(defaultPermissionUrl) - .then() - .statusCode(HttpStatus.SC_CREATED) - ; - } - public static List getUserPermissions(String username, String password, String repositoryType) { return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) .extract() From 63c874917f5fc355572f813d07ca787bf85650fd Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 2 Aug 2019 15:02:13 +0200 Subject: [PATCH 087/106] added singleton annotation to AuthorizationChangedEventProducer to be sure that it is not destroyed by the gc --- .../sonia/scm/security/AuthorizationChangedEventProducer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java index 66dbb51073..3f81377992 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java +++ b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java @@ -48,6 +48,8 @@ import sonia.scm.user.User; import sonia.scm.user.UserEvent; import sonia.scm.user.UserModificationEvent; +import javax.inject.Singleton; + /** * Receives all kinds of events, which affects authorization relevant data and fires an * {@link AuthorizationChangedEvent} if authorization data has changed. @@ -55,6 +57,7 @@ import sonia.scm.user.UserModificationEvent; * @author Sebastian Sdorra * @since 1.52 */ +@Singleton @EagerSingleton public class AuthorizationChangedEventProducer { From 254665099c2ccaa01bc993886aabbe654055bfad Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 6 Aug 2019 11:32:05 +0200 Subject: [PATCH 088/106] added parameter for extra docker tag --- Jenkinsfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index e817ac2ba1..62ccc23cc1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -13,6 +13,9 @@ node('docker') { // Keep only the last 10 build to preserve space buildDiscarder(logRotator(numToKeepStr: '10')), disableConcurrentBuilds() + parameters([ + string(name: 'dockerTag', trim: true, description: 'Extra Docker Tag for cloudogu/scm-manager image') + ]) ]) timeout(activity: true, time: 30, unit: 'MINUTES') { @@ -66,6 +69,9 @@ node('docker') { docker.withRegistry('', 'hub.docker.com-cesmarvin') { image.push(dockerImageTag) image.push('latest') + if (params.dockerTag) { + image.push(dockerTag) + } } } From 13b5bcbb295fa696dad2e9245d1c528ac46e6c8f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 6 Aug 2019 11:36:31 +0200 Subject: [PATCH 089/106] fixed broken build --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 62ccc23cc1..4faa57325d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ node('docker') { properties([ // Keep only the last 10 build to preserve space buildDiscarder(logRotator(numToKeepStr: '10')), - disableConcurrentBuilds() + disableConcurrentBuilds(), parameters([ string(name: 'dockerTag', trim: true, description: 'Extra Docker Tag for cloudogu/scm-manager image') ]) From 09068a55d47641b0f626eed07e3d653bf11c8a96 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 6 Aug 2019 13:02:57 +0200 Subject: [PATCH 090/106] push extra docker image tag with hash and version --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index 4faa57325d..f8a7be2ad9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -71,6 +71,7 @@ node('docker') { image.push('latest') if (params.dockerTag) { image.push(dockerTag) + image.push("2.0.0-${commitHash.substring(0,7)}-dev-${dockerTag}") } } } From c885385ca4e36cd7e9669015814bd9df94548c17 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 6 Aug 2019 13:32:31 +0200 Subject: [PATCH 091/106] fix null value as environment variable --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index f8a7be2ad9..37406886ba 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -70,8 +70,8 @@ node('docker') { image.push(dockerImageTag) image.push('latest') if (params.dockerTag) { - image.push(dockerTag) - image.push("2.0.0-${commitHash.substring(0,7)}-dev-${dockerTag}") + image.push(params.dockerTag) + image.push("2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}") } } } From 036a7594479f820ca2610796b058596a5367a8a2 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 6 Aug 2019 13:45:10 +0200 Subject: [PATCH 092/106] set defaultValue of dockerTag parameter to an empty string --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 37406886ba..3e35a31afe 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,7 +14,7 @@ node('docker') { buildDiscarder(logRotator(numToKeepStr: '10')), disableConcurrentBuilds(), parameters([ - string(name: 'dockerTag', trim: true, description: 'Extra Docker Tag for cloudogu/scm-manager image') + string(name: 'dockerTag', trim: true, defaultValue: "", description: 'Extra Docker Tag for cloudogu/scm-manager image') ]) ]) From 681d81a8fa1c0aeb08fb2aba0d53a3e1ffaab4d5 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 6 Aug 2019 13:49:04 +0200 Subject: [PATCH 093/106] set description for builds with dockerTag parameter --- Jenkinsfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 3e35a31afe..e36a78b385 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -71,7 +71,10 @@ node('docker') { image.push('latest') if (params.dockerTag) { image.push(params.dockerTag) - image.push("2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}") + + def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}" + currentBuild.description = newDockerTag + image.push(newDockerTag) } } } From 9f0ebe57cd7a08bac4afaac357fc2a20022015dd Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 6 Aug 2019 13:57:28 +0200 Subject: [PATCH 094/106] set default dockerTag to latest Jenkins treat every build parameter as environment variable and empty or null values lead to an error, see https://issues.jenkins-ci.org/browse/JENKINS-38608 --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index e36a78b385..1fc9786dd5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,7 +14,7 @@ node('docker') { buildDiscarder(logRotator(numToKeepStr: '10')), disableConcurrentBuilds(), parameters([ - string(name: 'dockerTag', trim: true, defaultValue: "", description: 'Extra Docker Tag for cloudogu/scm-manager image') + string(name: 'dockerTag', trim: true, defaultValue: "latest", description: 'Extra Docker Tag for cloudogu/scm-manager image') ]) ]) @@ -69,7 +69,7 @@ node('docker') { docker.withRegistry('', 'hub.docker.com-cesmarvin') { image.push(dockerImageTag) image.push('latest') - if (params.dockerTag) { + if (!"latest".equals(params.dockerTag)) { image.push(params.dockerTag) def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}" From ce882d2ee70952433b15d570dcc32847eef8bc24 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 6 Aug 2019 16:50:28 +0200 Subject: [PATCH 095/106] use single quotes for string without interpolation --- Jenkinsfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1fc9786dd5..36135cf4a4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,14 +7,14 @@ import com.cloudogu.ces.cesbuildlib.* node('docker') { // Change this as when we go back to default - necessary for proper SonarQube analysis - mainBranch = "2.0.0-m3" + mainBranch = '2.0.0-m3' properties([ // Keep only the last 10 build to preserve space buildDiscarder(logRotator(numToKeepStr: '10')), disableConcurrentBuilds(), parameters([ - string(name: 'dockerTag', trim: true, defaultValue: "latest", description: 'Extra Docker Tag for cloudogu/scm-manager image') + string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image') ]) ]) @@ -69,7 +69,7 @@ node('docker') { docker.withRegistry('', 'hub.docker.com-cesmarvin') { image.push(dockerImageTag) image.push('latest') - if (!"latest".equals(params.dockerTag)) { + if (!'latest'.equals(params.dockerTag)) { image.push(params.dockerTag) def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}" @@ -102,7 +102,7 @@ String mainBranch Maven setupMavenBuild() { // Keep this version number in sync with .mvn/maven-wrapper.properties - Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8") + Maven mvn = new MavenInDocker(this, '3.5.2-jdk-8') if (isMainBranch()) { // Release starts javadoc, which takes very long, so do only for certain branches From e9454bb77958292385058b56ca45f3ef210e0162 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 7 Aug 2019 07:21:30 +0200 Subject: [PATCH 096/106] revert defaultValue for dockerTag --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 36135cf4a4..5ca6c60234 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,7 +14,7 @@ node('docker') { buildDiscarder(logRotator(numToKeepStr: '10')), disableConcurrentBuilds(), parameters([ - string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image') + string(name: 'dockerTag', trim: true, description: 'Extra Docker Tag for cloudogu/scm-manager image') ]) ]) @@ -69,7 +69,7 @@ node('docker') { docker.withRegistry('', 'hub.docker.com-cesmarvin') { image.push(dockerImageTag) image.push('latest') - if (!'latest'.equals(params.dockerTag)) { + if (params.dockerTag) { image.push(params.dockerTag) def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}" From c34900aa30a8891cec8a9e663b7dbb0c97be0d45 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 7 Aug 2019 09:15:07 +0200 Subject: [PATCH 097/106] revert defaultValue change again --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 5ca6c60234..36135cf4a4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,7 +14,7 @@ node('docker') { buildDiscarder(logRotator(numToKeepStr: '10')), disableConcurrentBuilds(), parameters([ - string(name: 'dockerTag', trim: true, description: 'Extra Docker Tag for cloudogu/scm-manager image') + string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image') ]) ]) @@ -69,7 +69,7 @@ node('docker') { docker.withRegistry('', 'hub.docker.com-cesmarvin') { image.push(dockerImageTag) image.push('latest') - if (params.dockerTag) { + if (!'latest'.equals(params.dockerTag)) { image.push(params.dockerTag) def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}" From 074c0e51b3942e918f5d8d0beda04fa17cd40fe8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 7 Aug 2019 13:36:01 +0200 Subject: [PATCH 098/106] added scm-manager logo variations --- docs/logo/favicon_16x16px.ico | Bin 0 -> 1150 bytes docs/logo/favicon_16x16px_transparent.ico | Bin 0 -> 1150 bytes docs/logo/scm-manager_logo.ai | 1 + docs/logo/scm-manager_logo.jpg | Bin 0 -> 45667 bytes docs/logo/scm-manager_logo.png | Bin 0 -> 41610 bytes docs/logo/scm-manager_logo_img.jpg | Bin 0 -> 34748 bytes docs/logo/scm-manager_logo_img.png | Bin 0 -> 33758 bytes docs/logo/scm-manager_logo_img_neg.jpg | Bin 0 -> 25370 bytes docs/logo/scm-manager_logo_img_neg.png | Bin 0 -> 27443 bytes docs/logo/scm-manager_logo_neg.jpg | Bin 0 -> 47177 bytes docs/logo/scm-manager_logo_neg.png | Bin 0 -> 25105 bytes docs/logo/scm-manager_logo_neg1.jpg | Bin 0 -> 34398 bytes docs/logo/scm-manager_logo_neg1.png | Bin 0 -> 19272 bytes docs/logo/scm-manager_logo_pos1.jpg | Bin 0 -> 34444 bytes docs/logo/scm-manager_logo_pos1.png | Bin 0 -> 34079 bytes 15 files changed, 1 insertion(+) create mode 100644 docs/logo/favicon_16x16px.ico create mode 100644 docs/logo/favicon_16x16px_transparent.ico create mode 100644 docs/logo/scm-manager_logo.ai create mode 100644 docs/logo/scm-manager_logo.jpg create mode 100644 docs/logo/scm-manager_logo.png create mode 100644 docs/logo/scm-manager_logo_img.jpg create mode 100644 docs/logo/scm-manager_logo_img.png create mode 100644 docs/logo/scm-manager_logo_img_neg.jpg create mode 100644 docs/logo/scm-manager_logo_img_neg.png create mode 100644 docs/logo/scm-manager_logo_neg.jpg create mode 100644 docs/logo/scm-manager_logo_neg.png create mode 100644 docs/logo/scm-manager_logo_neg1.jpg create mode 100644 docs/logo/scm-manager_logo_neg1.png create mode 100644 docs/logo/scm-manager_logo_pos1.jpg create mode 100644 docs/logo/scm-manager_logo_pos1.png diff --git a/docs/logo/favicon_16x16px.ico b/docs/logo/favicon_16x16px.ico new file mode 100644 index 0000000000000000000000000000000000000000..3436795fdfcd58cab1d3edf3ac5593a8ca4ba7ec GIT binary patch literal 1150 zcmcIk&r1S96n;UsE<(^bA}Hvx*sY8G2SMk8j?q6*hYtP;MgKyFAWcJvAgV(qDQ+Pe z2wSPUxr({1D{lBJzUHlLb&=ddZ+Y`(-n{RdZ+3&NeqWKhd>Wc#Na^y!b6o7KLh(crqrPvx?uXet*@Sa^fN9J?Q z)|2?C+@Y2}fL%^H?#^-K|LtmLEOU7=*2;Tk4)+d?isw7)>ehAL(K0HQ>v&yW-?E0E lix)nxwM4c}WbjMSBFd@Gqd<-8LF85yIj!_Fc6&i^?+Xoq(Zm1% literal 0 HcmV?d00001 diff --git a/docs/logo/favicon_16x16px_transparent.ico b/docs/logo/favicon_16x16px_transparent.ico new file mode 100644 index 0000000000000000000000000000000000000000..e5803f340df8c825ba1344e189e192e34f983a9f GIT binary patch literal 1150 zcmcJNJ4?e*6vwY1D7X{^7l+0sX#^39F22Bg1N#x&baZzx;NY0i`oM@*P<$hbqBYf_ z;2>GF)!Le>^_33!01nsxX-#nuYX)!m_1xZbe&-(2Fjj|8Ai(e~u+e(PS{P&P07p2f zoFgz5x#p${^!vY7DLEMR+dGv?MSwtF$sCN9P6t%l`&sa}sUG*MPNLuP<5Der6O?Cn z5`FyrsN3+<1FivV5O5N`_S<0>f8Fo=bB`~dBkImckNJ8K;V*j~{CP()i<_cQYZiDk zKl%LOc*rQby9zIEN$%5n$Sth4Y4QR1&YB^pW zjsU#5$FQ$H6R+mCh!15P2(S&u?z4Hil8t7f{j9<0TCA`!Pz8ZlgLeh L1`y;UtsLM0i literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo.ai b/docs/logo/scm-manager_logo.ai new file mode 100644 index 0000000000..6fe98a15e0 --- /dev/null +++ b/docs/logo/scm-manager_logo.ai @@ -0,0 +1 @@ +%!PS-Adobe-2.0 %%Creator: Adobe Photoshop(TM) Pen Path Export 7.0 %%Title: (scm-manager_logo.ai) %%DocumentNeededResources: procset Adobe_packedarray 2.0 0 %%+ procset Adobe_IllustratorA_AI3 1.0 1 %%ColorUsage: Black&White %%BoundingBox: 0 0 800 275 %%HiResBoundingBox: 0 0 800 275 %AI3_Cropmarks: 0 0 800 275 %%DocumentPreview: None %%EndComments %%BeginProlog %%IncludeResource: procset Adobe_packedarray 2.0 0 Adobe_packedarray /initialize get exec %%IncludeResource: procset Adobe_IllustratorA_AI3 1.0 1 %%EndProlog %%BeginSetup Adobe_IllustratorA_AI3 /initialize get exec n %%EndSetup 0.0 0.0 0.0 1.0 k 0 i 0 J 0 j 1 w 4 M []0 d %%Note: %%Trailer %%EOF \ No newline at end of file diff --git a/docs/logo/scm-manager_logo.jpg b/docs/logo/scm-manager_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f518e3f2b22fd6bff3fa3a7ace5f7f6d613deb89 GIT binary patch literal 45667 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 bc)#@GfBp9UZ@~Yn!2ea?f3gA(eoy`fu!W$3Aq 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 e11;C2rZi<838oYu4 literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo_img.jpg b/docs/logo/scm-manager_logo_img.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2d3b35b66d5fda7a3b08f2ccbad39e12716042d GIT binary patch literal 34748 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 literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo_img.png b/docs/logo/scm-manager_logo_img.png new file mode 100644 index 0000000000000000000000000000000000000000..f349fc5d22efffe15ecbc5bd8f3b1bb1411dc952 GIT binary patch literal 33758 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

    -TdM%f<`we?~{IqXdzA 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 L0bC_!^zDBDZD?d` literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo_img_neg.jpg b/docs/logo/scm-manager_logo_img_neg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1b56b5e6272e96242b1430355292f64cefb42402 GIT binary patch literal 25370 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 literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo_img_neg.png b/docs/logo/scm-manager_logo_img_neg.png new file mode 100644 index 0000000000000000000000000000000000000000..796a02882c6ad5c0e01c7bed6a302ae324ab92d1 GIT binary patch literal 27443 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 literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo_neg.jpg b/docs/logo/scm-manager_logo_neg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d6704d0e39f8a232a4c41f3789cbeb2f31b2bf8 GIT binary patch literal 47177 zcmeFYRaBeX+9;eVZ7H<46)4^SAvgqGEmkze9SR{(BtQberK?4X7k5~Aad&H>xD^df zA)&ZKaQInk@4fbS@s0nSoAd39b7n@~G2eGSG9Q~huP3h;0n~DC)|LQ(q9P~Y0pQ>1 zdJ{k*i-y^{18xEC-h8$I0InDB?7nk$b`%4H?VWhcEF8??JTM0Y*v-rl%*VqE21rP| zIhw(2;m*&^;a1l6lFU1et<2A@EhL$Bg;aS}9cAF}tY3Se;F=z4S}+e=7{r2ETI#uk zo0uEI5dn8Ld+vs?vv(46lVtvjxY$kp&(C1y=YN4X+e$LO{FCc*J=HhQWgJlO=R!QZ zAQ-PO-*aI>9zH={h#-XfIX^ESFPM)X%qs%o;}sJU5#!~1{?EjG!wqF&DW)MS|4&{w zZ<5Ua6w1}rmB&?p#{p#p=7T^WU|xPOKR@UO0_5aw?`-ANXruj*UCz;HV_0&ef@bYmI5zZu==`ETI=md&3;Z)z?EMOoh% z(9BNO0fzpQjH0Y0Ga7AeAtoft&npVy7la7Bl9zqO#|PmPd?_jf;guEO6XF$>5&ai| z|B84+lE>l(S42>rUqqN+M4pdNh>uT>PgGFmB}7gRA|NZuCn6*y_b*&Udnaczdl>vr zrhnpE{~K5M{})$G1_d{Bc0g%4IN1HmRo=XFaCUHd=ivBUMpNjysu|4M{?GS6?f4H- zW#K4m7r2Ey$^r5GFGGk~|2O=3MFshV`Q!w6MfrJ!Mfvzd`T1W$LxlRz<)ZQo%IduU%7vvf&UA!zdrvLZ~YhKO$hv%{X6z!9 z1kY)M>luI_;MV_sy8Z8T=kN4S^xL;?-IU(^|1S97&0K#4 zkUY3Wdgu9_TO@$nB)9I6+`9e_n7wK8Zr}L_@xP=203JRfctCjn-kn=_Z&{)m#}Dp2ypi+vO~HMF2PC8qnaCcI^D$Grkuv24wo%+Jyd%X_XR93%5ReL0p3HHs|dq zELm3W3(EV;t!cHRK#wT`Op ztWZjPtf!-*q!IL)jeq}jki~D0Zh!*x>kM_;-I~*f zJ}fPnH1Z5%TC>MhX!L4qZVU+UXjZDWuUErH*pKt&9&uURGjF1CtbBi^zv=?M<$mWR z*MAu=Zs=X**4!iMc=l*PyfaD0Va1>p{uaM;6>MkvBe2naqq()cG&SGF6IgB|+%FG$WUD(UCyB?PLG^=+(4%W)P2SAQYU@kH#yh zpCymcpb3ZMoX@rDwt@8^mzH+r*YtTOKCkLL zztnSl=qPT$h#wo(3U82Ct0;dtqSTm-epz-@qe_8~C-zE#F^5T|dD;uxbqkYQ#0pld z6EL@mNJ&|oVD^Dcf<(s72}x<&J>syJ%FZc<8dbl(#)|of>-5Ld*)2DJoGKS#_`Dde z%c@m8RBmB;*9cA>uIV?;9M9WjU+(E@v*tik+)^B`%XelJo=cCHMA8R^@nes4tKuH3 z1Tx*th0D3u8ftv5`ttSQkN=F78=ia@=*?=N`mECeak-MMIpSdFNo^fQZ7z#B?zX(- zGnvug`&587>E|5yreO4GRY5~>;kSSp#bXYMtlQ()0M-av;64!BDiHq>wv6Z=ghMTiK=*!f9)D6aOzL@~lrH1+$1C-D0YC(6j? zs^HaVFdr%^51#3-i)Lf`P7QE_F;ah&%z^x2A2Yc6@LR=xKcLPT!?@oj^_1 zJ<*zxHHEROVs($U$60~z1n(O(`q~gGr5A^3Pb=y{Cf}p8oS~}Xu;Oe{cR5x_rGfv5 zc~2m+bJz2U;2z`3@$5q|6Tnn$-UqMZ`6f!K4{Ek5vrSY#)c=jS zrx1}t3OxWuRq}i~yc6@|KVnhS zJ>B$tUP`e-Kx`qG@{L5SgQ#VC0z5I(#;<$%>_ULE+&$~+G@rK>{+w8Sp50Bz&&et= z@fr|u*$kq+2EeyK(1tyF@g!Bq1D#4-Ty8+oHGnY^Nt7uxjjefF#uaSnW*qF^?KDv% z&L-G4a@T2Rj;yM#qaLrzcx=ff{%|@enX9th$;if?r2hVNc^k72tDny>Ox>EKGv{kU zPl3cFwCbEwQ;kXfKGT1~A-pd%f!&UFS#$$PI`_IO{lJCMWzmi!+uV*YcA1FDu9}?M zQm;<~iPbggwC*|A0FR{yU+F6^Tv3(x_N}pf3j4&~XrT_vkk{H}?N$_?TP^yjx4S}oHH@`8=*7ojDL+6#&1x<*!Z+@& zdnBwNgwCNmpl*nc-FJO2>*url>Mnj|oSYD&jqMV4#>37h-Ha)RV;{gi`s?G@RP(Dp zMy2GD39qweV#NvRu(?B`Bs8*i*MP^}$cuL7HiuGr1@po4yX75orRFcAQX+2;Kdvk3 zMn-*x%>F0*06=(l0d%ja>c;Iv6jjU))Eb8Lq>2!0WvvDph=o!)j=n@}x0DJup}xh@ z@~>?TIFGL90QeXOWP;Rwro3M&727>g!?0K{)fnhX*Oh3NV;H5zqcme|`vMmns)ik$ zJ}sz(js~f}NbLG7cbff{FnW*70%oG&rDBs(2}UN1Z5!=T>JV0W20f`tDP?U#2nEc9 zn*_6H``9uJSEZ}rk}Z6#0l=W$`N`A!LHmS*Qslo%8V#+ZZE1v^*6B5RV(ip}?Yek3 zUaMABGRsy?*x3qkjn)m5VTPsNzl})HdB~-53LmCB^Un&$f5Uc`^amR%3Xg4D<>Pta zzb##k@{CI|1Xskg0&^sreV^a%Yvs~zo17=w_uA*Oz-igF2yQ+)${{o`uwv=X)T&t0 zQPizbpaKb!##kuKjh1RM^FrLW5sc}pAt{{6(nV(k!VH&&?4BqDoTd=5n9i~A1A+2pmku}Oj z2pD{_Pp_mAlbD$3%*>P3=wGu<7n>G7CpSgqQ|uVCuqP+%ejcz|XOM~n*ewB>QDXx8*xtcBNPpjikh11ZImls2Ygr-GPuO z8P6LdG&4V`oxg1%o}5T3EIiZ>=TYupZ&Lku4HzLzOdGC0VQ&<9C@)7{!^YQQ%@bhD z!kSF)XVOy0vTFKvfT|{ZwQjwjZT-~8@}`MC@%8&Eeb%%&nhq_kJG`o0<{sTXy_q-- zt2TdesH#!>#TbpFW*a}$L{svW8Fl6fMrfRKDp3&+wuSXPWgb%M?@^)4-6!Ihk2yns zIu)VmUrPQsBSN^l)gRuh%y44z2WE9pyOsi+f?hXp%T{Yri2v!NTJAt+DdjCnI-0Qz zn+o5A_P+7YS5rS#R*NqfuIw{&BSw~QtA}q~NV5$%7$F?kvnaF6o6GTZJS9?JV#{$& z0d(L&gHKVWFlK#^7(X!$h95AtFA8Id2F`;Us)NM z@>ol^Hj!!6da`2euJZTG{bFRqf(&O^8NcE&;=?Kj|nmMof|TjdHtim5Cn}f-wojTt7zS ze{;s1^6z&GqCR`RsIjAORe z0fCoJdL>W1`nStZHZ!xR=je`wT&JM&%z7`ECnCxQn1tKt1Zxl?6#|wTe$J~nnnNF_(#!h4M#6SVi>Jsmc3?*bd*+{blqLd^ct+x7FlbzT!pOV@y}E8L6U z*b(t^BIfa|$8h!8n$oh*J&G>$OLXq4>fHq)<1=@aG?hwUQ5~@k*Hl1T6`tbs zB?4N2^4!bac1eMMR5^tvjdu<(#MXi7$fwcSXJ^hH=JhGZL2r@b^@An|$E6qy7lZP+ zt|-Syj!vPWcGQTF*jVg9*T}j?h>QA2n#f6h$E8lnN~F+-Zfyu-uAK4KxfjWO2|H) z+qk>8(8RP~7S&<4vElCltf#<5fDZ{sR#{{Rc``FkgC}M>j-;r+Q=#0S z8X#0L6&>7Yg_4dJiVwCBr-8JP=b?$p1w8G^^{uEA2gEMcWXn#+yx2p}`9-6V#+k;4 z5kbtZM!Jgg1t;`tDShSUHNa84zD{W?JgDiR-lf+?Jlm^iUriTDEAJMuIt)hAmoC^_ z_7+0z-2r2oyGodLE?-gbM~Z5psxV~XRrT6IA!Vi=Zu8k#G||dNgNKv@#Pm&+RETZ9 zD?J2fJi6`bdQ8w&*Gx59tG{?^XOah$S&8-?ne{cy$3O?A$=)^@R!SysvX3%o51`6R z=QxM2oJ4Zu1(c2X_*$~_&I&kZ19#)U&N7Ic82wH`F(vW>bu-h(vN&HY98E-v#vO_Y%7FG$vBF>F%^+ms67@v$(^hjD< zp(<1_a|Grg99%1&O#+bnCYEc>hM?!U%XUY;8LH|AFTFCfm|I>C)89-hgFOZk$0&40 zYKyVf=-6R1b*$R=piwpw5wo~CmR*7bAu2geLLK9qiMya|O8YE_J)U~#2}6(lscrjc z22iuKR~L#$cwGuP%tD(8rWere+#5r^x#EaEa}7p3_8i)LNuyzWMnM23I& zjEw!q2r-zp2)iv;DE>}R2BhO-%Nm}~DzZcNe$m8PjI6ayb2#>J)mx5##rRuciiPu{ zU9!#`jq1C-wacco7R_qsd|SLOz3O{BwuZ`Og{n~c^Nx5?7C}L8>*X{dl}UL- zPM^&Bx-Uah{p^b%{y=qOY!#|MjE3Qxrk)VA%3Dlo1UybT?Rj5eES8izJO__{cHvaf z+=J^CUfhZs4gw$5i^1BVNk@Vy@h6`n$CiB-Jr&w#^M~eEdj=!UQdChz7~Gt(_P5LE zlqVt0ZhSpy5}Dcot|eB39hr&8E7UgE0Hr-9VksAG zDbNvc%e08xEdBf~Hf_vHGr|$ga?m*E;FLd~uNP5+x%BWLwb_Ni$Kd zqT@?CJ6h4f5-*eR8EReMJR_6BxRYxFFS9Iq)4cQFwZe?4%Vy; zZE55Vs-igpPwN=_{J2M7rozEC1xW+FHybZCT!zGMSwdutdR~lk4o+wyS=elpw6}Qu z8)r8HjEe{UXYqc$lz)ya>iT2qcQqx+CW|k6|HUpBD)4uIZB#h!3IlbSoyJQz(b>7bL9qI ze6!2wVnetfu-aWnSmXo^weZ98rdFWeSkqWb>xwDTjnKQ3#7r3>j%_BzpOmXt?A?q| zB~$Ln?%N40XlD^tuL|cdrju^Vn!RRO5-V zRxF#)LEAC8NAs*_F`Ip!zzI$zIOwBMav=+A_CUb&8WVo!!=`cK(&@9(_yq$TB?c28 z$uYi`FR3F5?$nIQSrnm%d$$nkz4+Yf(wN^X+3l2~MmV!+Yf75C1HD5^m^I<31{Pp) zldqGftplz-w(&BFQ<9b3E?(~50iKt%YOZbCk2D%gK#lrcA8S>HsX~qV{Asqrs<$VV z{>U|_b7DrgWIc^bUy4skMt>ZD6* z2_3F$MBBSc`#btfdqRIspth}JwNv6IU(7|1j>DsUHQK-CUmAl-1ER`Hb=F>yDAf-w zIXn=ICn(FG(%N`r8|F8_vhQ>`9&-uO67v`nRIc<7hgdA~1uoe|(ugjl05uD(KOdboMKGV}Qd+4=Z}0Pj%@%@?PM65yaaPia+$o3*=i?PUO-3S-OQO zNR5TVK*x_bnQb52%epeKb$eR`6j}1;6VqG+#<4Zt5zn}0(@|8$&UMFjSfC-$S)%Ly zGEgKpp;~YuWLHOx2YUFtiCsT!z*DO0duJeMM*hXL2nZ7 zkJbr&w()xrqJ2v;HrrgXTj8cScXlRIPu7xUb1+mpe6}y;bEdlnf1x2uN~m0Y(AvDl zBh`%T0ZvP?25z?$vYGVfxGBO&SN8M0{03wDc%GuZUdlg)6YnMiR2Ju{}Pr@sH09!R5@eh;!4YTifj~%w))H zCkqS)Q8o{&S}EcNj`T%#Q8C#U@!c88;m1Sg)xAvi%l1CUJv-yNMldecXy1S&TVwKx z7ETgBs+9C(=8(vFE1_*pr}9COrI5*ERnPKk0Mv<+L&J zLT~DfhAdy&;CSHjhh3v*+7p5<_bFxV(~y{qSH7aUi-^``vG|kjTFENBVR!9oOuZ`y z2vG-BI5l#mLm8avJB+J>p?(xQKWFtRaYIrUAtuK;O~>ZLA_70ogg&iZz~y(;f`sVh zDTq$8eXs0}a-^@ATPE+Q?YY(p7z+p>$y<6I*Kda7%%7?+goO^^UtBzW@YwemK*YJd zXW&d^o$=yLzZT5~-nUV`3TA~VoG9h^eR6#kH2q{ok;NKQmTENBUaAzb!y|Yxfv3&s zeR*XQ400mC<6H66$)f4iMg|T1?j@cTK19Pohh5vTcm-Yjp)O+u+;N5BM8LtReQR)> z)@Dzc19Al2j6oK}tC;qUHJI8--L1Ca0k6`Njy|?Qs0Lxlk`N#tsicGYJ{&&pAiZ}R z1OE(ClGao*mGOoSvvT{={*(~^grVLWXryDnRUL_WZ2NkTu2-I9$4_bv8qP8Nbn1gk z88$92!&(`YOGmp*(G-W;7d)Vgslyukvyro)N?+?OFSQmeGdeDILZbx* z@HKGDM3$EBf}lcY$HAQr6%|}Z`_d+T&WoN2&$Sn{ovUdqY)r3R+PNd|cOYlaACHu33>s3v~XP%E3Z> z7)oWWPkYJUO5m1SvEYbVHHS|3Thj@~0$`NI5QZD}G&@f>)3UCz%8NL|hej0`IKR^|Wv(8js$L<{wB z6XPe5giQ@yiWHD_Sl=9p%ZMEhHijIZ<@O=6F`QyN4L&M%lqcvC&99;JEZyUSu&y}sJpdE|aw zS!%NA;4KkV&woM|Icgi+kr-!3&Qt|@XCq4=k!_5rR_TDe zQZf#;GuKz`mGx(JTTb_TclYo+HkT=3iF79PWqHC{rPJHuAyKHDx~ms$b9UW`!?XOC z)h5!Rj$6e+%}ovaQVfM-Bpk2?@S~La>XmnaQyHBvf|kFqbq&U|#~t@Jk}D;o z!*-uo(O0B{emRwLq^;;=Nvq}|@N47Go=KHb4;v28*(!nZ&@GP&up)w@3zS}Ok&7$v zNuh{gK?ME00$;)iUifGKw3&^);A>S-p4}#;d}heK9OipQFri{Z-ki5fI^mP|E{p+? zDIqoH3m3sOHTbj}Yh1X-rNKn(IK81YT6LJH-FX|+(1PnEpV)ULD9uqZ$_lK7K|pYz zq}KB$No9*v%)pBd#Y3GxW+Z2xHSdpCreO?fKIpLJyxXLSYXFx-K6%jjJi1ez-@7nI z@EgnX?o=gJ9k3+9(tK$j5`oNW;)c7>5 zznd~lOMen7HvMicJ@=>N_8l0W~szi zO`FW%9fDn!KK;_kitmm=rx}+kXSJ^%njdMsblU(Khxq{44hul@VC3vU)0F1Sc1)S* z$Fx|)p^~cpQiF-gh(=F&vtU5*m)38lx?@LKaq}iZ4C$yLqG9!oW3_hJLJ-Sym3Ym< z@}8jeXrXBtWPV~%?lY;}agXT$Z|T!^e!<5qbuHMF6usIQNSP>y9+7i&zcB-a5(Vbd=E9Pdy^P93F301^; zi^r*OA6;Jw&Unt;P6b^f@c7#bALwO}U@c{Qrg?QaHurI0f+FoRo7`vhA*&{*iI;mu zjwjau*T$_nP#>qxOf68br2b6Qxiq){=v>p3luMSpyWvZ^xNX!pb6h=;vpK1r8MgGs z01L~BtiuV$MYSM~L&M;HR9SoeIc1+~KK zS8BYw%Ddw#x>_ykKf4J+_ZVzvC8yEIre|9i=alM-?oOR|GZ(rz)kB%B*cx!tr{bAk ztp-0*8d3?1w*^kSg=z-%Qzd=@LtXKS%qkKdB;7qA>g}Vg&s*mGEn~sUz0G5GZuWW? z()VZkbU!pQHQ1bEjnFl=7gn0*ghAh$BT;rok6V1}HEVwyIxtdj?VQ87a&mZm?N~g{ zU&qW!$wSZ$#9D@_g;t;5D6uG&huK8@@Q8YMb_8D&;$gq)$RRo8j~9MP<9i&u+|xKW zo2EXlql2{+CT|@#ziOlPO8$7n+S-*8;@;j|hY$2Wob#_5Ppnh8^nqOrBwdWznZz(2 zpnn@tbf&tKhuHA)d2rTh(CccJhN|9`$)2LOx$7ftJRgWL&l!f}yI2%T*; zUbWWnE_b$ieW@=)dy3i?*)*5OG+hGXu^bs_)?**FKZqs|5>-+``+<8e8<=zWmNsp9 z=p^6mOWPi=XrnZN7^=GmsROPwSe`b7vG(Z9s=T-4gU2fe28H%rrf%a~N-gCBi*+{G zV%8jdt+is|<=8op|5z)}T+qVQA@9}sZw#ZqD3rlwW-Xjt<83n{rA&4Rg)K zf|82vn06RYD-9U;##W5PoY5SazLKu}LihVn@xc2HX)fOSTXE%-Ch@zGOB>cGVikAd z?2L4#dMFI6zOMRC(Jt(<>GFvbrQAIw{1{ZxYUW&}o7iY7Ks(K$xHiUeifkVhmRw&0 z8icyCJ=Dm-K?d7#AUHKAnyOIH(M57rvl>H!gkovKwniz;X<#lS*^%JGi+w@nEP^Z&FBz5Ve%F ztF8NJ-7oqYplGByA@UkS?6h6{ai~OA!;vF+9lqrrJexyzhowZlJVbvQ#v|NO+?46;@r4DO9T4Ty#Y47C05J0eiZ>!C>xk z?rKAA$%jE?WfxT8=e}FZS3f;Vau^ijWiE0A_F5WtRNLz0O~4|MOHGH)29xL*-ICX- z)$ueJibxL%Tg8i4ZMq(rFYT5(hHBF~?76HH;XrRjtw*1X*Vq~|QEY-_%C9*Xkw72I zH0T5T={K0h*>-%4PVmKMcx+L6x(ZdtNn3(7#W;J+hOwykkK^Cx6mg1YZhNBz4h4G2 zCXAYEe!79iQNwE#If=p>Kv4|k#wYKWUExaZmebn~&F5&VkE;gx6#m6M@>e{Iz~!%5C+DQ%4V5po_OrX8W!gL8d&G!svd=C0eZ^^DvJ!h%dIh&2&#GeQ=Y87qL?-+el6K$;g zI4NY_yNZ+)WVzhrNZh|?vtBvcHeINyEoWP>@vCXpd_lWLnz+$&Om|I-1=MZ~Y<6TxVuYBg&Xuo1!#FP8}5&F!hITuYllSV2?ntE zDwME^$$M;t_=h?%6PXMv@(BW5PuNSdcTLR^^0i_uAT^K zvCnP&-55UZ-*hLDM$Bv}i!(zABiD$6V;-v)_MUCSXI!Se6IZH|Y@Vi&FPLjUAeg`O z*mFLecX_`SzSU$mCAu*IW~w^a4sWo@t`6WfIMG?T8A2c!sgt)_PK~=vUJT1wSy{$R z&L&iE)kbW1S#sMxZcq{a0VfrbqS4CreOR;_rR<=1_Tu3o*sI!FrhSLrepgY0+==&0CBp&-yxX(Hf#tZ8gMPvRBSyx1QGgsc6fV z>?!ib&^1w zpl>qw)$p78()xL56J&As3|9S9`iYK#VKm#py6tG8X$%Vx>>tqXX}g*1x3`K(pXCnP z@yr}Gxp_3>s&H+_Oy|jIDftd#4OjVAzek-`uD~AOPR_Y=hEFHrw8k+<0=KD zDmW&knU%BUvsUFx{7n3R_}dN5x)NbH-DgtqILmg&~+6#E^bWK>H`L}?n z3aolitxlD{7KU~s_?+o(W^2gzgZ5mH=l>J^+YJx0qrH!lL$;;DMiQ$lwS;0$4jaj= zN#~>+5UJQn#z5iH%GPVZ=FfERO=4cF1>7K4Tbk{6dfvu8r6fyCJytDg9 zRUsM9B(yeI(VbO^t>TwKHOG$ks8r4<<_k^ha^Y{G=N4`zlQ(kK_dgm8 zTYG2jbz~>2N9HSgvb?)l5*I1WxXcFh&TArTATReNc00PPh=QD-*+B-9T}EK%GJ zCz6-5j@DGER&It>j1@)8Xe(?xW%y1EkDbu~xY$-SQ`580c3@`IbA@>+ryyKPi5+4* zN3GUOvpw}Cg@HmBdr=)5N9AN+^%V`vJ8=?4L?@Z2&T)Y$;1xJ*UE*E6fMJcmm(}Rf zlVN|$i->+2VmeWU>BW%%PmbH^`xXid$Yak!fJ&8g@9&S}1Bk9P-l>J1L3yd7Z&(I@ht z8kM+%Y4;UkQD4;nUGfcLH}?m<#Wf6X=gb)*qp_7WMsYN;w1h9**O0$pTKGt0l8#16 z*c5~h??HF}Rr|i=HNbi67!YtzQe)eft3`Kaw6LP~^N0GIAD{={b}-;jM>TWor=q7Jkq=H z&z|iv`DS~1^rmV|ax)`0d*gz0ZZWN6rClW^21hgCd*oSz=x+D)#;cw2PPY%KUJAq- zdY9G(<$EzPh*^G(Yk;BahmzKgtddWcYCV$FSTIG+PIOo!M}kB%sQ}}*faF=`QDCf%^LDU2Sof`AoFH0RyTwE)A3n7&O_4*p>DPK~S8Y=i{ z4fGSuhsTxAfPu;}+BOX7s=EZ$el2yFtlZ_USB1r%dAGG=7xm5GO)O#tkY0H znJljDZ4SpAO^}ktSi+r8E``ai(desbr_JQmscq~09&5x8OdIuh^{6)2q2&b~Ruhtm*wWrr8$~u# zE!Q*$v8%xERlD0aW3ZmNtw^SLd5J*?i-j>CrffbpI99o^jYaB%)VMIJ z=yhmJ{G_fH1$3LSz8Q;lnh|}*_Is^cUbJAE3MQ`1gf~O_qp^;c*3S!~4=%n_RP>WD zo#Q!Mvxe;}Qa$Z@ZqD?yxW2VYPQ}x_wb7rWB^^tzBs&H=<*daHo>x~iHB^1Z^@)21 zjTCKh!yCMfW5v8MCb2v+fv0^)8}%zq3M&pS4iRQ|_c*oLLbjqj2t-Nsv|dTI=4#>y zY)MH>ITK`Ze@<2JL`Y7a0z=(aC+9BD8>}8|dN)d8;2FWY|Dpcjh7B7_oBVIv0?R{P zs>xg$j;2j4WL`zvazv4`*r>q5hf zP@Qo&S88W6kX32ZOldfZjz!%isafoZ_kP&EZ)Ba6Kpzxx^}2GStFSCZiGgH^Lw;^d z`svOfk7Te0F6Dc~$Ns1TUzvE`Bb3eBa`|jR?bI_U`n6)=7jlw?^GXoT1UGanZ;(fD$MA}YA>_1WV#0Ii>BTCr2Q=?KMb!>{J!0!`TfI}djf^)0@}Y` z_Zo$eJ?yRw<@f2YNF08ev8Ev)P+XQ zAfHifV8SFjyQPIhJ2DmPB4`^UJ-a+kt#IO7*rconItmecBT}zrwNFYO`irpN3vf}N zRY1Nj@XLdBC_NZEx;X8%Yo3@Q1K$plsEpb+-d{#&R>08h_w-L}a$~SuRi_=c)N3nV zo3`ufT)M6t(KGW;Dm*o}4ows|2el0w#eC^c^@;hzc?uUH&b^2e1s5WHF1S%bE@3xnHIY(g%0Ag$~;I>ySYsrKLZ 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 literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo_neg1.jpg b/docs/logo/scm-manager_logo_neg1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d776c6b6e67de60b873206279baaad91b5ff8fcb GIT binary patch literal 34398 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 aaz5*{TA{O6Nj|TIjUcS;t;iNW34a36kSve@ literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo_pos1.jpg b/docs/logo/scm-manager_logo_pos1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fd2533c198c68353b4e978b6acb536fb4e6f09a2 GIT binary patch literal 34444 zcmeFYWmH_x(l0!OBv=R#fxm*5V;9bW$D zId<>+th?@q^L)EKYi8BnUG=N(s;=tZy?dT!pH>0*l5Un}0D!D40{|KDukv&Nz>;t_ zv33U_0A4=dHU$8lRuTVLI62w#v$EPcvKX1#8G~6&>}*)wjOxf2-y%(C3!(D?=@x9cToR zurqP~3r1E#h{D;~(v+V|f}M?*kDZf`Lrh9S3<%@{a*FbD@v%v80J+$B#d-e$@b7}p zkSwOpvh1P~Y<&%`;T|BZe&QBHPlpd<$yFFPAIFA&Jf&MwL)CCbgm&Bx2h!N*PUcUjYa zGuS`+{X8uH&$6t~jWIRhcd~SXfd4*1MIlaqry!PpjS_x{k*)c&IhaksW=75sCkkN+ z=yP-mvi>#rAePU(f200OjP>8Z{&xG{M(aN~pF`j;@}IHyeDTld1=~J{Ec7`xpAG>s z|1uy43mbq}OjhybU-r%GRnm~Y2Dlq5r!gmz zmh1l05#a6LVi#cg@c)(nXMz6_3!q-P z&nKV84cxYn?5|t9%kl<|SFZR@c%|BZSFP87yIV#se7K%or@%DiJakmWC2v)AW^RPR z@)HG_Q|^J1+mU$t-B&-U6m~c9te5-Sg^2P>IaZ4?7e!=jsNq!79rT1ikMnwhW)u{Z?1T#KeqY2(rAKOl!@Y|R# z5@w_$j0=Lv=`l*0gT>}zG+uaWDR)56-ZOWUUIx4u3`kH(*j&i|s93Zp6ObDUISEGSZtDCB{b+fkrS6&l2rgfar*>xdAM)+ID%)H4f z6>Y+^IqpMjZDPSY$@pLRC^r9Qvi}JZ0j@vNlM>vA#CUk5yfn6DkW|NLEzO-`5i6aZ zqpOt`%Q+ONsiGGjqE+M~HMG(|e-=-?O34ic>6ACiy=Eri84+0TjnX?1La)?_%|8BM zKW9PJGo3Fg7oYcL zjJ%5^w4E(1g6jv1RXW;zR!OD_!y$JGqr;7$g(P%EhIUsxR@M5F+Lkqn?-P=kTVQd8 zANs-Qz=|_zbFG4wF2hsX3op+flx@yjMt)j(zAe2t)X=}fwW_VLMz2zi35gYqf6eS{ z9#a#uB|A-0)tQ~|h~*Ti8r0rS+OkCn7l~?nHPP+*!R9NyJ~gR(7aOHnSA`t|h2p9U z5k!jH=?wFv9-WoewwR~{yCjFGxxPBaq`~k_|6Gy=#@U8p zN~#nBvuzbY{>WI166cAFtL$cJh3lW8kk2Ca{}Vp^7v?K=9|Ipov{A1^j15iYnK#=G z&_xkp@F{gvx^9gL)|6;7&EPLoW{pJ4>B=LsysYsRmg`a_CRAF)^_

    }hHBCa` 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

    28wbN&WdC<^|c{)t*K5lj?WLEbT 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 literal 0 HcmV?d00001 diff --git a/docs/logo/scm-manager_logo_pos1.png b/docs/logo/scm-manager_logo_pos1.png new file mode 100644 index 0000000000000000000000000000000000000000..b91141735848806ec1dcf37d8d54f1e05038df11 GIT binary patch literal 34079 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

    k<;UTG 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 Date: Wed, 7 Aug 2019 16:46:01 +0200 Subject: [PATCH 099/106] added support information on admin overview page --- scm-ui/public/images/iconCommunitySupport.png | Bin 0 -> 3653 bytes .../public/images/iconEnterpriseSupport.png | Bin 0 -> 3928 bytes scm-ui/public/locales/de/admin.json | 11 ++- scm-ui/public/locales/en/admin.json | 13 +++- scm-ui/src/admin/containers/AdminDetails.js | 65 ++++++++++++++++-- 5 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 scm-ui/public/images/iconCommunitySupport.png create mode 100644 scm-ui/public/images/iconEnterpriseSupport.png diff --git a/scm-ui/public/images/iconCommunitySupport.png b/scm-ui/public/images/iconCommunitySupport.png new file mode 100644 index 0000000000000000000000000000000000000000..7a080903a0b29278563b9a435ba3f7e77d71fb2f GIT binary patch literal 3653 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 literal 0 HcmV?d00001 diff --git a/scm-ui/public/images/iconEnterpriseSupport.png b/scm-ui/public/images/iconEnterpriseSupport.png new file mode 100644 index 0000000000000000000000000000000000000000..0eadfaf85b63afbee2fe97a07a632486a8ddb5a4 GIT binary patch literal 3928 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 zqBh5sKontaktieren Sie das SCM-Manager Support Team bei unserem Entwicklungs-Partner Cloudogu für ein unverbindliches Angebot.", + "enterpriseLink": "https://cloudogu.com/de/scm-manager-enterprise/", + "enterpriseButton": "Enterprise Support anfragen" } }, "plugins": { diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index a5697ea4fe..86b3e788e0 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -6,8 +6,17 @@ "settingsNavLink": "Settings", "generalNavLink": "General" }, - "information": { - "currentAppVersion": "Current Application Version" + "info": { + "currentAppVersion": "Current Application Version", + "communityTitle": "Community Support", + "communityIconAlt": "Community Support Icon", + "communityInfo": "The Cloudogu support team is available for general questions about SCM-Manager, bug reporting and feature requests through our official channels.", + "communityButton": "Contact our team", + "enterpriseTitle": "Enterprise Support", + "enterpriseIconAlt": "Enterprise Support Icon", + "enterpriseInfo": "You need technical support for your company or you need a SCM-Manager plugin developed for you? We are happy to help you with your individual requirements for SCM-Manager use.
    Contact the SCM-Manager Support Team at our development partner Cloudogu for a non-binding offer.", + "enterpriseLink": "https://cloudogu.com/en/scm-manager-enterprise/", + "enterpriseButton": "Request Enterprise Support" } }, "plugins": { diff --git a/scm-ui/src/admin/containers/AdminDetails.js b/scm-ui/src/admin/containers/AdminDetails.js index a7a135aa97..4b98a891e5 100644 --- a/scm-ui/src/admin/containers/AdminDetails.js +++ b/scm-ui/src/admin/containers/AdminDetails.js @@ -1,8 +1,15 @@ // @flow import React from "react"; import { connect } from "react-redux"; +import injectSheet from "react-jss"; import { translate } from "react-i18next"; -import { Loading, Title, Subtitle } from "@scm-manager/ui-components"; +import classNames from "classnames"; +import { + Loading, + Title, + Subtitle, + Image +} from "@scm-manager/ui-components"; import { getAppVersion } from "../../modules/indexResource"; type Props = { @@ -11,13 +18,23 @@ type Props = { version: string, - // context objects + // context props + classes: any, t: string => string }; +const styles = { + boxShadow: { + boxShadow: "0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 1px rgb(40, 177, 232)" + }, + boxTitle: { + fontWeight: "500 !important" + } +}; + class AdminDetails extends React.Component { render() { - const { t, loading } = this.props; + const { loading, classes, t } = this.props; if (loading) { return ; @@ -25,8 +42,46 @@ class AdminDetails extends React.Component { return ( <> - + <Title title={t("admin.info.currentAppVersion")} /> <Subtitle subtitle={this.props.version} /> + <div className={classNames("box", classes.boxShadow)}> + <article className="media"> + <div className="media-left"> + <figure className="image is-64x64"> + <Image + src="/images/iconCommunitySupport.png" + alt={t("admin.info.communityIconAlt")} + /> + </figure> + </div> + <div className="media-content"> + <div className="content"> + <h3 className={classes.boxTitle}>{t("admin.info.communityTitle")}</h3> + <p>{t("admin.info.communityInfo")}</p> + <a className="button is-info is-pulled-right" href="https://scm-manager.org/support/">{t("admin.info.communityButton")}</a> + </div> + </div> + </article> + </div> + <div className={classNames("box", classes.boxShadow)}> + <article className="media"> + <div className="media-left"> + <figure className="image is-64x64"> + <Image + src="/images/iconEnterpriseSupport.png" + alt={t("admin.info.enterpriseIconAlt")} + /> + </figure> + </div> + <div className="media-content"> + <div className="content"> + <h3 className={classes.boxTitle}>{t("admin.info.enterpriseTitle")}</h3> + <p>{t("admin.info.enterpriseInfo")}</p> + <a className="button is-info is-pulled-right" href={t("admin.info.enterpriseLink")}>{t("admin.info.enterpriseButton")}</a> + </div> + </div> + </article> + </div> </> ); } @@ -39,4 +94,4 @@ const mapStateToProps = (state: any) => { }; }; -export default connect(mapStateToProps)(translate("admin")(AdminDetails)); +export default connect(mapStateToProps)(injectSheet(styles)(translate("admin")(AdminDetails))); From 454453ea4cc4634fdea52455c70855dff1e5d009 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 7 Aug 2019 17:01:29 +0200 Subject: [PATCH 100/106] added second translation for partner information to not have to change i18n.options.react to adapt behaviour or defining var --- scm-ui/public/locales/de/admin.json | 3 ++- scm-ui/public/locales/en/admin.json | 3 ++- scm-ui/src/admin/containers/AdminDetails.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index 6d3371c1cd..c03dbf9b01 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -14,7 +14,8 @@ "communityButton": "Unser Team kontaktieren", "enterpriseTitle": "Enterprise Support", "enterpriseIconAlt": "Enterprise Support Icon", - "enterpriseInfo": "Sie benötigen technische Unterstützung für Ihr Unternehmen oder haben Bedarf an einem für Sie entwickelten SCM-Manager Plugin? Gerne helfen wir Ihnen bei Ihren individuellen Anforderungen für die SCM-Manager Nutzung weiter.<br /><strong>Kontaktieren Sie das SCM-Manager Support Team bei unserem Entwicklungs-Partner Cloudogu für ein unverbindliches Angebot.</strong>", + "enterpriseInfo": "Sie benötigen technische Unterstützung für Ihr Unternehmen oder haben Bedarf an einem für Sie entwickelten SCM-Manager Plugin? Gerne helfen wir Ihnen bei Ihren individuellen Anforderungen für die SCM-Manager Nutzung weiter.", + "enterprisePartner": "Kontaktieren Sie das SCM-Manager Support Team bei unserem Entwicklungs-Partner Cloudogu für ein unverbindliches Angebot.", "enterpriseLink": "https://cloudogu.com/de/scm-manager-enterprise/", "enterpriseButton": "Enterprise Support anfragen" } diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index 86b3e788e0..21e3ed4e84 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -14,7 +14,8 @@ "communityButton": "Contact our team", "enterpriseTitle": "Enterprise Support", "enterpriseIconAlt": "Enterprise Support Icon", - "enterpriseInfo": "You need technical support for your company or you need a SCM-Manager plugin developed for you? We are happy to help you with your individual requirements for SCM-Manager use.<br /><strong>Contact the SCM-Manager Support Team at our development partner Cloudogu for a non-binding offer.</strong>", + "enterpriseInfo": "You need technical support for your company or you need a SCM-Manager plugin developed for you? We are happy to help you with your individual requirements for SCM-Manager use.", + "enterprisePartner": "Contact the SCM-Manager Support Team at our development partner Cloudogu for a non-binding offer.", "enterpriseLink": "https://cloudogu.com/en/scm-manager-enterprise/", "enterpriseButton": "Request Enterprise Support" } diff --git a/scm-ui/src/admin/containers/AdminDetails.js b/scm-ui/src/admin/containers/AdminDetails.js index 4b98a891e5..99b448cda3 100644 --- a/scm-ui/src/admin/containers/AdminDetails.js +++ b/scm-ui/src/admin/containers/AdminDetails.js @@ -76,7 +76,7 @@ class AdminDetails extends React.Component<Props> { <div className="media-content"> <div className="content"> <h3 className={classes.boxTitle}>{t("admin.info.enterpriseTitle")}</h3> - <p>{t("admin.info.enterpriseInfo")}</p> + <p>{t("admin.info.enterpriseInfo")}<br /><strong>{t("admin.info.enterprisePartner")}</strong></p> <a className="button is-info is-pulled-right" href={t("admin.info.enterpriseLink")}>{t("admin.info.enterpriseButton")}</a> </div> </div> From 2dfeabfd0e9feb312f9bf422a37336ddfeff0d5c Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 7 Aug 2019 17:21:46 +0200 Subject: [PATCH 101/106] small typo key fix --- scm-ui/public/locales/de/admin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index c03dbf9b01..5744ad6c6d 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -6,7 +6,7 @@ "settingsNavLink": "Einstellungen", "generalNavLink": "Generell" }, - "information": { + "info": { "currentAppVersion": "Aktuelle Software-Versionsnummer", "communityTitle": "Community Support", "communityIconAlt": "Community Support Icon", From 74df156ba0a7c0ba69efee18d2a86e1c6c414e92 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 8 Aug 2019 10:40:38 +0200 Subject: [PATCH 102/106] open links in new tab / small ui fixes --- scm-ui/src/admin/containers/AdminDetails.js | 23 ++++++++------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/scm-ui/src/admin/containers/AdminDetails.js b/scm-ui/src/admin/containers/AdminDetails.js index 99b448cda3..e439077731 100644 --- a/scm-ui/src/admin/containers/AdminDetails.js +++ b/scm-ui/src/admin/containers/AdminDetails.js @@ -1,16 +1,11 @@ // @flow import React from "react"; -import { connect } from "react-redux"; +import {connect} from "react-redux"; import injectSheet from "react-jss"; -import { translate } from "react-i18next"; +import {translate} from "react-i18next"; import classNames from "classnames"; -import { - Loading, - Title, - Subtitle, - Image -} from "@scm-manager/ui-components"; -import { getAppVersion } from "../../modules/indexResource"; +import {Image, Loading, Subtitle, Title} from "@scm-manager/ui-components"; +import {getAppVersion} from "../../modules/indexResource"; type Props = { loading: boolean, @@ -25,7 +20,7 @@ type Props = { const styles = { boxShadow: { - boxShadow: "0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 1px rgb(40, 177, 232)" + boxShadow: "0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 2px rgb(40, 177, 232, 0.2)" }, boxTitle: { fontWeight: "500 !important" @@ -47,7 +42,7 @@ class AdminDetails extends React.Component<Props> { <div className={classNames("box", classes.boxShadow)}> <article className="media"> <div className="media-left"> - <figure className="image is-64x64"> + <figure className="image is-96x96"> <Image src="/images/iconCommunitySupport.png" alt={t("admin.info.communityIconAlt")} @@ -58,7 +53,7 @@ class AdminDetails extends React.Component<Props> { <div className="content"> <h3 className={classes.boxTitle}>{t("admin.info.communityTitle")}</h3> <p>{t("admin.info.communityInfo")}</p> - <a className="button is-info is-pulled-right" href="https://scm-manager.org/support/">{t("admin.info.communityButton")}</a> + <a className="button is-info is-pulled-right" target="_blank" href="https://scm-manager.org/support/">{t("admin.info.communityButton")}</a> </div> </div> </article> @@ -66,7 +61,7 @@ class AdminDetails extends React.Component<Props> { <div className={classNames("box", classes.boxShadow)}> <article className="media"> <div className="media-left"> - <figure className="image is-64x64"> + <figure className="image is-96x96"> <Image src="/images/iconEnterpriseSupport.png" alt={t("admin.info.enterpriseIconAlt")} @@ -77,7 +72,7 @@ class AdminDetails extends React.Component<Props> { <div className="content"> <h3 className={classes.boxTitle}>{t("admin.info.enterpriseTitle")}</h3> <p>{t("admin.info.enterpriseInfo")}<br /><strong>{t("admin.info.enterprisePartner")}</strong></p> - <a className="button is-info is-pulled-right" href={t("admin.info.enterpriseLink")}>{t("admin.info.enterpriseButton")}</a> + <a className="button is-info is-pulled-right is-normal" target="_blank" href={t("admin.info.enterpriseLink")}>{t("admin.info.enterpriseButton")}</a> </div> </div> </article> From 358f7475e722790364461fe60bf49b8ae6f90d4f Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 8 Aug 2019 11:12:46 +0200 Subject: [PATCH 103/106] change translations --- scm-ui/public/locales/de/admin.json | 6 +++--- scm-ui/public/locales/en/admin.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index 5744ad6c6d..54ae1ab91a 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -10,12 +10,12 @@ "currentAppVersion": "Aktuelle Software-Versionsnummer", "communityTitle": "Community Support", "communityIconAlt": "Community Support Icon", - "communityInfo": "Das Cloudogu Support-Team steht für allgemeine Fragen rund um SCM-Manager, die Meldung von Fehlern sowie Anfragen von Features gerne für Sie über unsere offiziellen Kanäle bereit.", + "communityInfo": "Das SCM-Manager Support-Team steht für allgemeine Fragen, die Meldung von Fehlern sowie Anfragen für Features gerne für Sie über die offiziellen Kanäle bereit.", "communityButton": "Unser Team kontaktieren", "enterpriseTitle": "Enterprise Support", "enterpriseIconAlt": "Enterprise Support Icon", - "enterpriseInfo": "Sie benötigen technische Unterstützung für Ihr Unternehmen oder haben Bedarf an einem für Sie entwickelten SCM-Manager Plugin? Gerne helfen wir Ihnen bei Ihren individuellen Anforderungen für die SCM-Manager Nutzung weiter.", - "enterprisePartner": "Kontaktieren Sie das SCM-Manager Support Team bei unserem Entwicklungs-Partner Cloudogu für ein unverbindliches Angebot.", + "enterpriseInfo": "Sie benötigen Unterstützung bei der Integration von SCM-Manager in Ihre Prozesse, bei der Anpassung des Tools auf Ihre Anforderungen oder einfach ein Service Level Agreement (SLA)?", + "enterprisePartner": "Treten Sie mit unserem Entwicklungs-Partner Cloudogu in Kontakt! Das Team freut sich auf den Austausch über Ihre individuellen Anforderungen und erstellt Ihnen gerne ein maßgeschneidertes Angebot.", "enterpriseLink": "https://cloudogu.com/de/scm-manager-enterprise/", "enterpriseButton": "Enterprise Support anfragen" } diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index 21e3ed4e84..2402f21423 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -10,12 +10,12 @@ "currentAppVersion": "Current Application Version", "communityTitle": "Community Support", "communityIconAlt": "Community Support Icon", - "communityInfo": "The Cloudogu support team is available for general questions about SCM-Manager, bug reporting and feature requests through our official channels.", + "communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.", "communityButton": "Contact our team", "enterpriseTitle": "Enterprise Support", "enterpriseIconAlt": "Enterprise Support Icon", - "enterpriseInfo": "You need technical support for your company or you need a SCM-Manager plugin developed for you? We are happy to help you with your individual requirements for SCM-Manager use.", - "enterprisePartner": "Contact the SCM-Manager Support Team at our development partner Cloudogu for a non-binding offer.", + "enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?", + "enterprisePartner": "Contact our development partner Cloudogu! Their team is looking forward to discussing your individual requirements with you and will be more than happy to give you a quote.", "enterpriseLink": "https://cloudogu.com/en/scm-manager-enterprise/", "enterpriseButton": "Request Enterprise Support" } From 50499adbe4e3fe1f69da1fa4492d04fe8e3063fd Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 9 Aug 2019 08:35:14 +0200 Subject: [PATCH 104/106] fixed iconSize and imagePadding --- scm-ui/src/admin/containers/AdminDetails.js | 43 +++++++++++---------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/scm-ui/src/admin/containers/AdminDetails.js b/scm-ui/src/admin/containers/AdminDetails.js index e439077731..b257d04554 100644 --- a/scm-ui/src/admin/containers/AdminDetails.js +++ b/scm-ui/src/admin/containers/AdminDetails.js @@ -24,55 +24,56 @@ const styles = { }, boxTitle: { fontWeight: "500 !important" + }, + imagePadding: { + padding: "0.5rem 0.5rem" } }; class AdminDetails extends React.Component<Props> { render() { - const { loading, classes, t } = this.props; + const {loading, classes, t} = this.props; if (loading) { - return <Loading />; + return <Loading/>; } return ( <> - <Title title={t("admin.info.currentAppVersion")} /> - <Subtitle subtitle={this.props.version} /> + <Title title={t("admin.info.currentAppVersion")}/> + <Subtitle subtitle={this.props.version}/> <div className={classNames("box", classes.boxShadow)}> <article className="media"> - <div className="media-left"> - <figure className="image is-96x96"> - <Image - src="/images/iconCommunitySupport.png" - alt={t("admin.info.communityIconAlt")} - /> - </figure> + <div className={classNames("media-left", classes.imagePadding)}> + <Image + src="/images/iconCommunitySupport.png" + alt={t("admin.info.communityIconAlt")} + /> </div> <div className="media-content"> <div className="content"> <h3 className={classes.boxTitle}>{t("admin.info.communityTitle")}</h3> <p>{t("admin.info.communityInfo")}</p> - <a className="button is-info is-pulled-right" target="_blank" href="https://scm-manager.org/support/">{t("admin.info.communityButton")}</a> + <a className="button is-info is-pulled-right" target="_blank" + href="https://scm-manager.org/support/">{t("admin.info.communityButton")}</a> </div> </div> </article> </div> <div className={classNames("box", classes.boxShadow)}> <article className="media"> - <div className="media-left"> - <figure className="image is-96x96"> - <Image - src="/images/iconEnterpriseSupport.png" - alt={t("admin.info.enterpriseIconAlt")} - /> - </figure> + <div className={classNames("media-left", classes.imagePadding)}> + <Image + src="/images/iconEnterpriseSupport.png" + alt={t("admin.info.enterpriseIconAlt")} + /> </div> <div className="media-content"> <div className="content"> <h3 className={classes.boxTitle}>{t("admin.info.enterpriseTitle")}</h3> - <p>{t("admin.info.enterpriseInfo")}<br /><strong>{t("admin.info.enterprisePartner")}</strong></p> - <a className="button is-info is-pulled-right is-normal" target="_blank" href={t("admin.info.enterpriseLink")}>{t("admin.info.enterpriseButton")}</a> + <p>{t("admin.info.enterpriseInfo")}<br/><strong>{t("admin.info.enterprisePartner")}</strong></p> + <a className="button is-info is-pulled-right is-normal" target="_blank" + href={t("admin.info.enterpriseLink")}>{t("admin.info.enterpriseButton")}</a> </div> </div> </article> From 3f6f45cebccf71c4775bc6771b4d5e82da6c256d Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 9 Aug 2019 08:40:31 +0200 Subject: [PATCH 105/106] adjust imagePadding --- scm-ui/src/admin/containers/AdminDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/admin/containers/AdminDetails.js b/scm-ui/src/admin/containers/AdminDetails.js index b257d04554..803524abed 100644 --- a/scm-ui/src/admin/containers/AdminDetails.js +++ b/scm-ui/src/admin/containers/AdminDetails.js @@ -26,7 +26,7 @@ const styles = { fontWeight: "500 !important" }, imagePadding: { - padding: "0.5rem 0.5rem" + padding: "0.2rem 0.4rem" } }; From 3822d2c4c35aeec918e97e6efc3cdfa5c5974919 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Fri, 9 Aug 2019 06:46:37 +0000 Subject: [PATCH 106/106] Close branch feature/admin_support