Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2018-10-18 08:50:49 +02:00
49 changed files with 2702 additions and 9318 deletions

View File

@@ -19,13 +19,14 @@
"flow-bin": "^0.79.1", "flow-bin": "^0.79.1",
"flow-typed": "^2.5.1", "flow-typed": "^2.5.1",
"jest": "^23.5.0", "jest": "^23.5.0",
"raf": "^3.4.0" "raf": "^3.4.0",
"react-router-enzyme-context": "^1.2.0"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"moment": "^2.22.2", "moment": "^2.22.2",
"react": "^16.4.2", "react": "^16.5.2",
"react-dom": "^16.4.2", "react-dom": "^16.5.2",
"react-i18next": "^7.11.0", "react-i18next": "^7.11.0",
"react-jss": "^8.6.1", "react-jss": "^8.6.1",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",

View File

@@ -0,0 +1,133 @@
//@flow
import React from "react";
import {translate} from "react-i18next";
import type {PagedCollection} from "@scm-manager/ui-types";
import {Button} from "./buttons";
type Props = {
collection: PagedCollection,
page: number,
// context props
t: string => string
};
class LinkPaginator extends React.Component<Props> {
renderFirstButton() {
return (
<Button
className={"pagination-link"}
label={"1"}
disabled={false}
link={"1"}
/>
);
}
renderPreviousButton(label?: string) {
const { page } = this.props;
const previousPage = page - 1;
return (
<Button
className={"pagination-previous"}
label={label ? label : previousPage.toString()}
disabled={!this.hasLink("prev")}
link={`${previousPage}`}
/>
);
}
hasLink(name: string) {
const { collection } = this.props;
return collection._links[name];
}
renderNextButton(label?: string) {
const { page } = this.props;
const nextPage = page + 1;
return (
<Button
className={"pagination-next"}
label={label ? label : nextPage.toString()}
disabled={!this.hasLink("next")}
link={`${nextPage}`}
/>
);
}
renderLastButton() {
const { collection } = this.props;
return (
<Button
className={"pagination-link"}
label={`${collection.pageTotal}`}
disabled={false}
link={`${collection.pageTotal}`}
/>
);
}
separator() {
return <span className="pagination-ellipsis">&hellip;</span>;
}
currentPage(page: number) {
return (
<Button
className="pagination-link is-current"
label={page}
disabled={true}
/>
);
}
pageLinks() {
const { collection } = this.props;
const links = [];
const page = collection.page + 1;
const pageTotal = collection.pageTotal;
if (page > 1) {
links.push(this.renderFirstButton());
}
if (page > 3) {
links.push(this.separator());
}
if (page > 2) {
links.push(this.renderPreviousButton());
}
links.push(this.currentPage(page));
if (page + 1 < pageTotal) {
links.push(this.renderNextButton());
}
if (page + 2 < pageTotal)
//if there exists pages between next and last
links.push(this.separator());
if (page < pageTotal) {
links.push(this.renderLastButton());
}
return links;
}
render() {
const { t } = this.props;
return (
<nav className="pagination is-centered" aria-label="pagination">
{this.renderPreviousButton(t("paginator.previous"))}
<ul className="pagination-list">
{this.pageLinks().map((link, index) => {
return <li key={index}>{link}</li>;
})}
</ul>
{this.renderNextButton(t("paginator.next"))}
</nav>
);
}
}
export default translate("commons")(LinkPaginator);

View File

@@ -18,8 +18,10 @@ class Paginator extends React.Component<Props> {
createAction = (linkType: string) => () => { createAction = (linkType: string) => () => {
const { collection, onPageChange } = this.props; const { collection, onPageChange } = this.props;
if (onPageChange) { if (onPageChange) {
const link = collection._links[linkType].href; const link = collection._links[linkType];
onPageChange(link); if (link && link.href) {
onPageChange(link.href);
}
} }
}; };

View File

@@ -3,10 +3,13 @@ import React from "react";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import "./tests/enzyme"; import "./tests/enzyme";
import "./tests/i18n"; import "./tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
import Paginator from "./Paginator"; import Paginator from "./Paginator";
describe("paginator rendering tests", () => { describe("paginator rendering tests", () => {
const options = new ReactRouterEnzymeContext();
const dummyLink = { const dummyLink = {
href: "https://dummy" href: "https://dummy"
}; };
@@ -18,7 +21,10 @@ describe("paginator rendering tests", () => {
_links: {} _links: {}
}; };
const paginator = shallow(<Paginator collection={collection} />); const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(7); expect(buttons.length).toBe(7);
for (let button of buttons) { for (let button of buttons) {
@@ -37,7 +43,10 @@ describe("paginator rendering tests", () => {
} }
}; };
const paginator = shallow(<Paginator collection={collection} />); const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(5); expect(buttons.length).toBe(5);
@@ -73,7 +82,10 @@ describe("paginator rendering tests", () => {
} }
}; };
const paginator = shallow(<Paginator collection={collection} />); const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(6); expect(buttons.length).toBe(6);
@@ -112,7 +124,10 @@ describe("paginator rendering tests", () => {
} }
}; };
const paginator = shallow(<Paginator collection={collection} />); const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(5); expect(buttons.length).toBe(5);
@@ -148,7 +163,10 @@ describe("paginator rendering tests", () => {
} }
}; };
const paginator = shallow(<Paginator collection={collection} />); const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(6); expect(buttons.length).toBe(6);
@@ -189,7 +207,10 @@ describe("paginator rendering tests", () => {
} }
}; };
const paginator = shallow(<Paginator collection={collection} />); const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(7); expect(buttons.length).toBe(7);
@@ -244,7 +265,8 @@ describe("paginator rendering tests", () => {
}; };
const paginator = mount( const paginator = mount(
<Paginator collection={collection} onPageChange={callMe} /> <Paginator collection={collection} onPageChange={callMe} />,
options.get()
); );
paginator.find("Button.pagination-previous").simulate("click"); paginator.find("Button.pagination-previous").simulate("click");

View File

@@ -1,7 +1,7 @@
//@flow //@flow
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Link } from "react-router-dom"; import { withRouter } from "react-router-dom";
export type ButtonProps = { export type ButtonProps = {
label: string, label: string,
@@ -16,7 +16,10 @@ export type ButtonProps = {
type Props = ButtonProps & { type Props = ButtonProps & {
type: string, type: string,
color: string color: string,
// context prop
history: any
}; };
class Button extends React.Component<Props> { class Button extends React.Component<Props> {
@@ -25,14 +28,22 @@ class Button extends React.Component<Props> {
color: "default" color: "default"
}; };
renderButton = () => { onClick = (event: Event) => {
const { action, link, history } = this.props;
if (action) {
action(event);
} else if (link) {
history.push(link);
}
};
render() {
const { const {
label, label,
loading, loading,
disabled, disabled,
type, type,
color, color,
action,
fullWidth, fullWidth,
className className
} = this.props; } = this.props;
@@ -42,7 +53,7 @@ class Button extends React.Component<Props> {
<button <button
type={type} type={type}
disabled={disabled} disabled={disabled}
onClick={action ? action : (event: Event) => {}} onClick={this.onClick}
className={classNames( className={classNames(
"button", "button",
"is-" + color, "is-" + color,
@@ -56,14 +67,6 @@ class Button extends React.Component<Props> {
); );
}; };
render() {
const { link } = this.props;
if (link) {
return <Link to={link}>{this.renderButton()}</Link>;
} else {
return this.renderButton();
}
}
} }
export default Button; export default withRouter(Button);

View File

@@ -15,9 +15,11 @@ export { default as Logo } from "./Logo.js";
export { default as MailLink } from "./MailLink.js"; export { default as MailLink } from "./MailLink.js";
export { default as Notification } from "./Notification.js"; export { default as Notification } from "./Notification.js";
export { default as Paginator } from "./Paginator.js"; export { default as Paginator } from "./Paginator.js";
export { default as LinkPaginator } from "./LinkPaginator.js";
export { default as ProtectedRoute } from "./ProtectedRoute.js"; export { default as ProtectedRoute } from "./ProtectedRoute.js";
export { default as Help } from "./Help.js"; export { default as Help } from "./Help.js";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js"; export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
export { getPageFromMatch } from "./urls";
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js"; export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";

View File

@@ -7,7 +7,8 @@ import { Route, Link } from "react-router-dom";
type Props = { type Props = {
to: string, to: string,
label: string, label: string,
activeOnlyWhenExact?: boolean activeOnlyWhenExact?: boolean,
activeWhenMatch?: (route: any) => boolean
}; };
class NavLink extends React.Component<Props> { class NavLink extends React.Component<Props> {
@@ -15,11 +16,17 @@ class NavLink extends React.Component<Props> {
activeOnlyWhenExact: true activeOnlyWhenExact: true
}; };
isActive(route: any) {
const { activeWhenMatch } = this.props;
return route.match || (activeWhenMatch && activeWhenMatch(route));
}
renderLink = (route: any) => { renderLink = (route: any) => {
const { to, label } = this.props; const { to, label } = this.props;
return ( return (
<li> <li>
<Link className={route.match ? "is-active" : ""} to={to}> <Link className={this.isActive(route) ? "is-active" : ""} to={to}>
{label} {label}
</Link> </Link>
</li> </li>

View File

@@ -1,7 +1,7 @@
// @flow // @flow
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
import { getProtocolLinkByType, getTypePredicate } from "./repositories"; import { getProtocolLinkByType } from "./repositories";
describe("getProtocolLinkByType tests", () => { describe("getProtocolLinkByType tests", () => {

View File

@@ -4,3 +4,11 @@ export const contextPath = window.ctxPath || "";
export function withContextPath(path: string) { export function withContextPath(path: string) {
return contextPath + path; return contextPath + path;
} }
export function getPageFromMatch(match: any) {
let page = parseInt(match.params.page, 10);
if (isNaN(page) || !page) {
page = 1;
}
return page;
}

View File

@@ -0,0 +1,27 @@
// @flow
import { getPageFromMatch } from "./urls";
describe("tests for getPageFromMatch", () => {
function createMatch(page: string) {
return {
params: {
page
}
};
}
it("should return 1 for NaN", () => {
const match = createMatch("any");
expect(getPageFromMatch(match)).toBe(1);
});
it("should return 1 for 0", () => {
const match = createMatch("0");
expect(getPageFromMatch(match)).toBe(1);
});
it("should return the given number", () => {
const match = createMatch("42");
expect(getPageFromMatch(match)).toBe(42);
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
//@flow
import type {Links} from "./hal";
export type Branch = {
name: string,
revision: string,
_links: Links
}

View File

@@ -0,0 +1,25 @@
//@flow
import type {Links} from "./hal";
import type {Tag} from "./Tags";
import type {Branch} from "./Branches";
export type Changeset = {
id: string,
date: Date,
author: {
name: string,
mail?: string
},
description: string,
_links: Links,
_embedded: {
tags?: Tag[],
branches?: Branch[],
parents?: ParentChangeset[]
};
}
export type ParentChangeset = {
id: string,
_links: Links
}

View File

@@ -0,0 +1,8 @@
//@flow
import type { Links } from "./hal";
export type Tag = {
name: string,
revision: string,
_links: Links
}

View File

@@ -4,10 +4,14 @@ export type Link = {
name?: string name?: string
}; };
export type Links = { [string]: Link | Link[] }; type LinkValue = Link | Link[];
// TODO use LinkValue
export type Links = { [string]: any };
export type Collection = { export type Collection = {
_embedded: Object, _embedded: Object,
// $FlowFixMe
_links: Links _links: Links
}; };

View File

@@ -9,6 +9,12 @@ export type { Group, Member } from "./Group";
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories"; export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes"; export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
export type { Branch } from "./Branches";
export type { Changeset } from "./Changesets";
export type { Tag } from "./Tags";
export type { Config } from "./Config"; export type { Config } from "./Config";
export type { Permission, PermissionEntry, PermissionCollection } from "./RepositoryPermissions"; export type { Permission, PermissionEntry, PermissionCollection } from "./RepositoryPermissions";

View File

@@ -16,9 +16,8 @@
"i18next-browser-languagedetector": "^2.2.2", "i18next-browser-languagedetector": "^2.2.2",
"i18next-fetch-backend": "^0.1.0", "i18next-fetch-backend": "^0.1.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"node-sass": "^4.9.3", "react": "^16.5.2",
"react": "^16.4.2", "react-dom": "^16.5.2",
"react-dom": "^16.4.2",
"react-i18next": "^7.9.0", "react-i18next": "^7.9.0",
"react-jss": "^8.6.0", "react-jss": "^8.6.0",
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
@@ -51,6 +50,7 @@
"fetch-mock": "^6.5.0", "fetch-mock": "^6.5.0",
"flow-typed": "^2.5.1", "flow-typed": "^2.5.1",
"jest": "^23.5.0", "jest": "^23.5.0",
"node-sass": "^4.9.3",
"node-sass-chokidar": "^1.3.0", "node-sass-chokidar": "^1.3.0",
"npm-run-all": "^4.1.3", "npm-run-all": "^4.1.3",
"prettier": "^1.13.7", "prettier": "^1.13.7",

View File

@@ -22,6 +22,7 @@
"actions-label": "Actions", "actions-label": "Actions",
"back-label": "Back", "back-label": "Back",
"navigation-label": "Navigation", "navigation-label": "Navigation",
"history": "Commits",
"information": "Information", "information": "Information",
"permissions": "Permissions", "permissions": "Permissions",
"sources": "Sources" "sources": "Sources"
@@ -53,31 +54,49 @@
"description": "Description" "description": "Description"
} }
}, },
"changesets": {
"error-title": "Error",
"error-subtitle": "Could not fetch changesets",
"changeset": {
"id": "ID",
"description": "Description",
"contact": "Contact",
"date": "Date",
"summary": "Changeset {{id}} committed {{time}}"
},
"author": {
"name": "Author",
"mail": "Mail"
}
},
"branch-selector": {
"label": "Branches"
},
"permission": { "permission": {
"error-title": "Error", "error-title": "Error",
"error-subtitle": "Unknown permissions error", "error-subtitle": "Unknown permissions error",
"name": "User or Group", "name": "User or Group",
"type": "Type", "type": "Type",
"group-permission": "Group Permission", "group-permission": "Group Permission",
"edit-permission": { "edit-permission": {
"delete-button": "Delete", "delete-button": "Delete",
"save-button": "Save Changes" "save-button": "Save Changes"
}, },
"delete-permission-button": { "delete-permission-button": {
"label": "Delete", "label": "Delete",
"confirm-alert": { "confirm-alert": {
"title": "Delete permission", "title": "Delete permission",
"message": "Do you really want to delete the permission?", "message": "Do you really want to delete the permission?",
"submit": "Yes", "submit": "Yes",
"cancel": "No" "cancel": "No"
}
},
"add-permission": {
"add-permission-heading": "Add new Permission",
"submit-button": "Submit",
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
} }
}, },
"add-permission": {
"add-permission-heading": "Add new Permission",
"submit-button": "Submit",
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
}
},
"help": { "help": {
"nameHelpText": "The name of the repository. This name will be part of the repository url.", "nameHelpText": "The name of the repository. This name will be part of the repository url.",
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).", "typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",

View File

@@ -1,14 +1,13 @@
//@flow //@flow
import React from "react"; import React from "react";
import { Route, Redirect, withRouter } from "react-router"; import { Redirect, Route, Switch, withRouter } from "react-router-dom";
import Overview from "../repos/containers/Overview"; import Overview from "../repos/containers/Overview";
import Users from "../users/containers/Users"; import Users from "../users/containers/Users";
import Login from "../containers/Login"; import Login from "../containers/Login";
import Logout from "../containers/Logout"; import Logout from "../containers/Logout";
import { Switch } from "react-router-dom";
import { ProtectedRoute } from "@scm-manager/ui-components"; import { ProtectedRoute } from "@scm-manager/ui-components";
import AddUser from "../users/containers/AddUser"; import AddUser from "../users/containers/AddUser";
import SingleUser from "../users/containers/SingleUser"; import SingleUser from "../users/containers/SingleUser";

View File

@@ -7,6 +7,7 @@ import { routerReducer, routerMiddleware } from "react-router-redux";
import users from "./users/modules/users"; import users from "./users/modules/users";
import repos from "./repos/modules/repos"; import repos from "./repos/modules/repos";
import repositoryTypes from "./repos/modules/repositoryTypes"; import repositoryTypes from "./repos/modules/repositoryTypes";
import changesets from "./repos/modules/changesets";
import sources from "./repos/sources/modules/sources"; import sources from "./repos/sources/modules/sources";
import groups from "./groups/modules/groups"; import groups from "./groups/modules/groups";
import auth from "./modules/auth"; import auth from "./modules/auth";
@@ -16,6 +17,7 @@ import permissions from "./repos/permissions/modules/permissions";
import config from "./config/modules/config"; import config from "./config/modules/config";
import type { BrowserHistory } from "history/createBrowserHistory"; import type { BrowserHistory } from "history/createBrowserHistory";
import branches from "./repos/modules/branches";
function createReduxStore(history: BrowserHistory) { function createReduxStore(history: BrowserHistory) {
const composeEnhancers = const composeEnhancers =
@@ -28,6 +30,8 @@ function createReduxStore(history: BrowserHistory) {
users, users,
repos, repos,
repositoryTypes, repositoryTypes,
changesets,
branches,
permissions, permissions,
groups, groups,
auth, auth,

View File

@@ -0,0 +1,40 @@
// @flow
import React from "react";
import classNames from "classnames";
type Props = {
options: string[],
optionSelected: string => void,
preselectedOption?: string,
className: any
};
class DropDown extends React.Component<Props> {
render() {
const { options, preselectedOption, className } = this.props;
return (
<div className={classNames(className, "select")}>
<select
value={preselectedOption ? preselectedOption : ""}
onChange={this.change}
>
<option key="" />
{options.map(option => {
return (
<option key={option} value={option}>
{option}
</option>
);
})}
</select>
</div>
);
}
change = (event: SyntheticInputEvent<HTMLSelectElement>) => {
this.props.optionSelected(event.target.value);
};
}
export default DropDown;

View File

@@ -4,7 +4,6 @@ import "../../tests/enzyme";
import "../../tests/i18n"; import "../../tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context"; import ReactRouterEnzymeContext from "react-router-enzyme-context";
import PermissionsNavLink from "./PermissionsNavLink"; import PermissionsNavLink from "./PermissionsNavLink";
import EditNavLink from "./EditNavLink";
describe("PermissionsNavLink", () => { describe("PermissionsNavLink", () => {
const options = new ReactRouterEnzymeContext(); const options = new ReactRouterEnzymeContext();

View File

@@ -0,0 +1,37 @@
//@flow
import React from "react";
import type { Changeset } from "@scm-manager/ui-types";
type Props = {
changeset: Changeset
};
export default class ChangesetAuthor extends React.Component<Props> {
render() {
const { changeset } = this.props;
if (!changeset.author) {
return null;
}
const { name } = changeset.author;
return (
<>
{name} {this.renderMail()}
</>
);
}
renderMail() {
const { mail } = this.props.changeset.author;
if (mail) {
return (
<a className="is-hidden-mobile" href={"mailto:" + mail}>
&lt;
{mail}
&gt;
</a>
);
}
}
}

View File

@@ -0,0 +1,30 @@
//@flow
import React from "react";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import type { Changeset } from "@scm-manager/ui-types";
type Props = {
changeset: Changeset
};
class ChangesetAvatar extends React.Component<Props> {
render() {
const { changeset } = this.props;
return (
<ExtensionPoint
name="repos.changeset-table.information"
renderAll={true}
props={{ changeset }}
>
{/* extension should render something like this: */}
{/* <div className="image is-64x64"> */}
{/* <figure className="media-left"> */}
{/* <Image src="/some/image.jpg" alt="Logo" /> */}
{/* </figure> */}
{/* </div> */}
</ExtensionPoint>
);
}
}
export default ChangesetAvatar;

View File

@@ -0,0 +1,25 @@
//@flow
import { Link } from "react-router-dom";
import React from "react";
import type { Repository, Changeset } from "@scm-manager/ui-types";
type Props = {
repository: Repository,
changeset: Changeset
};
export default class ChangesetId extends React.Component<Props> {
render() {
const { repository, changeset } = this.props;
return (
<Link
to={`/repo/${repository.namespace}/${repository.name}/changeset/${
changeset.id
}`}
>
{changeset.id.substr(0, 7)}
</Link>
);
}
}

View File

@@ -0,0 +1,28 @@
// @flow
import ChangesetRow from "./ChangesetRow";
import React from "react";
import type { Changeset, Repository } from "@scm-manager/ui-types";
import classNames from "classnames";
type Props = {
repository: Repository,
changesets: Changeset[]
};
class ChangesetList extends React.Component<Props> {
render() {
const { repository, changesets } = this.props;
const content = changesets.map(changeset => {
return (
<ChangesetRow
key={changeset.id}
repository={repository}
changeset={changeset}
/>
);
});
return <div className={classNames("box")}>{content}</div>;
}
}
export default ChangesetList;

View File

@@ -0,0 +1,90 @@
//@flow
import React from "react";
import type { Changeset, Repository, Tag } from "@scm-manager/ui-types";
import classNames from "classnames";
import { translate, Interpolate } from "react-i18next";
import ChangesetAvatar from "./ChangesetAvatar";
import ChangesetId from "./ChangesetId";
import injectSheet from "react-jss";
import { DateFromNow } from "@scm-manager/ui-components";
import ChangesetAuthor from "./ChangesetAuthor";
import ChangesetTag from "./ChangesetTag";
import { compose } from "redux";
const styles = {
pointer: {
cursor: "pointer"
},
changesetGroup: {
marginBottom: "1em"
},
withOverflow: {
overflow: "auto"
}
};
type Props = {
repository: Repository,
changeset: Changeset,
t: any,
classes: any
};
class ChangesetRow extends React.Component<Props> {
createLink = (changeset: Changeset) => {
const { repository } = this.props;
return <ChangesetId changeset={changeset} repository={repository} />;
};
getTags = () => {
const { changeset } = this.props;
return changeset._embedded.tags || [];
};
render() {
const { changeset, classes } = this.props;
const changesetLink = this.createLink(changeset);
const dateFromNow = <DateFromNow date={changeset.date} />;
const authorLine = <ChangesetAuthor changeset={changeset} />;
return (
<article className={classNames("media", classes.inner)}>
<ChangesetAvatar changeset={changeset} />
<div className={classNames("media-content", classes.withOverflow)}>
<div className="content">
<p className="is-ellipsis-overflow">
{changeset.description}
<br />
<Interpolate
i18nKey="changesets.changeset.summary"
id={changesetLink}
time={dateFromNow}
/>
</p>{" "}
<div className="is-size-7">{authorLine}</div>
</div>
</div>
{this.renderTags()}
</article>
);
}
renderTags = () => {
const tags = this.getTags();
if (tags.length > 0) {
return (
<div className="media-right">
{tags.map((tag: Tag) => {
return <ChangesetTag key={tag.name} tag={tag} />;
})}
</div>
);
}
return null;
};
}
export default compose(
injectSheet(styles),
translate("repos")
)(ChangesetRow);

View File

@@ -0,0 +1,32 @@
//@flow
import React from "react";
import type { Tag } from "@scm-manager/ui-types";
import injectSheet from "react-jss";
import classNames from "classnames";
const styles = {
spacing: {
marginRight: "4px"
}
};
type Props = {
tag: Tag,
// context props
classes: Object
};
class ChangesetTag extends React.Component<Props> {
render() {
const { tag, classes } = this.props;
return (
<span className="tag is-info">
<span className={classNames("fa", "fa-tag", classes.spacing)} />{" "}
{tag.name}
</span>
);
}
}
export default injectSheet(styles)(ChangesetTag);

View File

@@ -1,9 +1,9 @@
//@flow //@flow
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import {Link} from "react-router-dom";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import type { Repository } from "@scm-manager/ui-types"; import type {Repository} from "@scm-manager/ui-types";
import { DateFromNow } from "@scm-manager/ui-components"; import {DateFromNow} from "@scm-manager/ui-components";
import RepositoryEntryLink from "./RepositoryEntryLink"; import RepositoryEntryLink from "./RepositoryEntryLink";
import classNames from "classnames"; import classNames from "classnames";
import RepositoryAvatar from "./RepositoryAvatar"; import RepositoryAvatar from "./RepositoryAvatar";
@@ -45,7 +45,7 @@ class RepositoryEntry extends React.Component<Props> {
return ( return (
<RepositoryEntryLink <RepositoryEntryLink
iconClass="fa-code-branch" iconClass="fa-code-branch"
to={repositoryLink + "/changesets"} to={repositoryLink + "/history"}
/> />
); );
} }
@@ -67,10 +67,7 @@ class RepositoryEntry extends React.Component<Props> {
renderModifyLink = (repository: Repository, repositoryLink: string) => { renderModifyLink = (repository: Repository, repositoryLink: string) => {
if (repository._links["update"]) { if (repository._links["update"]) {
return ( return (
<RepositoryEntryLink <RepositoryEntryLink iconClass="fa-cog" to={repositoryLink + "/edit"} />
iconClass="fa-cog"
to={repositoryLink + "/modify"}
/>
); );
} }
return null; return null;

View File

@@ -0,0 +1,139 @@
// @flow
import React from "react";
import type { Branch, Repository } from "@scm-manager/ui-types";
import { Route, withRouter } from "react-router-dom";
import Changesets from "./Changesets";
import BranchSelector from "./BranchSelector";
import { connect } from "react-redux";
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
import {
fetchBranches,
getBranches,
getFetchBranchesFailure,
isFetchBranchesPending
} from "../modules/branches";
import { compose } from "redux";
type Props = {
repository: Repository,
baseUrl: string,
selected: string,
baseUrlWithBranch: string,
baseUrlWithoutBranch: string,
// State props
branches: Branch[],
loading: boolean,
error: Error,
// Dispatch props
fetchBranches: Repository => void,
// Context props
history: any, // TODO flow type
match: any
};
class BranchRoot extends React.Component<Props> {
componentDidMount() {
this.props.fetchBranches(this.props.repository);
}
stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
return url.substring(0, url.length - 1);
}
return url;
};
branchSelected = (branch?: Branch) => {
let url;
if (branch) {
url = `${this.props.baseUrlWithBranch}/${encodeURIComponent(
branch.name
)}/changesets/`;
} else {
url = `${this.props.baseUrlWithoutBranch}/`;
}
this.props.history.push(url);
};
findSelectedBranch = () => {
const { selected, branches } = this.props;
return branches.find((branch: Branch) => branch.name === selected);
};
render() {
const { repository, error, loading, match, branches } = this.props;
if (error) {
return <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
if (!repository || !branches) {
return null;
}
const url = this.stripEndingSlash(match.url);
const branch = this.findSelectedBranch();
const changesets = <Changesets repository={repository} branch={branch} />;
return (
<>
{this.renderBranchSelector()}
<Route path={`${url}/:page?`} component={() => changesets} />
</>
);
}
renderBranchSelector = () => {
const { repository, branches } = this.props;
if (repository._links.branches) {
return (
<BranchSelector
branches={branches}
selected={(b: Branch) => {
this.branchSelected(b);
}}
/>
);
}
return null;
};
}
const mapDispatchToProps = dispatch => {
return {
fetchBranches: (repo: Repository) => {
dispatch(fetchBranches(repo));
}
};
};
const mapStateToProps = (state: any, ownProps: Props) => {
const { repository, match } = ownProps;
const loading = isFetchBranchesPending(state, repository);
const error = getFetchBranchesFailure(state, repository);
const branches = getBranches(state, repository);
const selected = decodeURIComponent(match.params.branch);
return {
loading,
error,
branches,
selected
};
};
export default compose(
withRouter,
connect(
mapStateToProps,
mapDispatchToProps
)
)(BranchRoot);

View File

@@ -0,0 +1,78 @@
// @flow
import React from "react";
import type { Branch } from "@scm-manager/ui-types";
import DropDown from "../components/DropDown";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
import { compose } from "redux";
import classNames from "classnames";
const styles = {
zeroflex: {
flexGrow: 0
}
};
type Props = {
branches: Branch[], // TODO: Use generics?
selected: (branch?: Branch) => void,
// context props
classes: Object,
t: string => string
};
type State = { selectedBranch?: Branch };
class BranchSelector extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {};
}
render() {
const { branches, classes, t } = this.props;
if (branches) {
return (
<div className="box field is-horizontal">
<div
className={classNames("field-label", "is-normal", classes.zeroflex)}
>
<label className="label">{t("branch-selector.label")}</label>
</div>
<div className="field-body">
<div className="field is-narrow">
<div className="control">
<DropDown
className="is-fullwidth"
options={branches.map(b => b.name)}
optionSelected={this.branchSelected}
preselectedOption={
this.state.selectedBranch
? this.state.selectedBranch.name
: ""
}
/>
</div>
</div>
</div>
</div>
);
}
}
branchSelected = (branchName: string) => {
const { branches, selected } = this.props;
const branch = branches.find(b => b.name === branchName);
selected(branch);
this.setState({ selectedBranch: branch });
};
}
export default compose(
injectSheet(styles),
translate("repos")
)(BranchSelector);

View File

@@ -0,0 +1,115 @@
// @flow
import React from "react";
import { withRouter } from "react-router-dom";
import type {
Branch,
Changeset,
PagedCollection,
Repository
} from "@scm-manager/ui-types";
import {
fetchChangesets,
getChangesets,
getFetchChangesetsFailure,
isFetchChangesetsPending,
selectListAsCollection
} from "../modules/changesets";
import { connect } from "react-redux";
import ChangesetList from "../components/changesets/ChangesetList";
import {
ErrorNotification,
LinkPaginator,
Loading,
getPageFromMatch
} from "@scm-manager/ui-components";
import { compose } from "redux";
type Props = {
repository: Repository,
branch: Branch,
page: number,
// State props
changesets: Changeset[],
list: PagedCollection,
loading: boolean,
error: Error,
// Dispatch props
fetchChangesets: (Repository, Branch, number) => void,
// context props
match: any
};
class Changesets extends React.Component<Props> {
componentDidMount() {
const { fetchChangesets, repository, branch, page } = this.props;
fetchChangesets(repository, branch, page);
}
render() {
const { changesets, loading, error } = this.props;
if (error) {
return <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
if (!changesets || changesets.length === 0) {
return null;
}
return (
<>
{this.renderList()}
{this.renderPaginator()}
</>
);
}
renderList = () => {
const { repository, changesets } = this.props;
return <ChangesetList repository={repository} changesets={changesets} />;
};
renderPaginator = () => {
const { page, list } = this.props;
if (list) {
return <LinkPaginator page={page} collection={list} />;
}
return null;
};
}
const mapDispatchToProps = dispatch => {
return {
fetchChangesets: (repo: Repository, branch: Branch, page: number) => {
dispatch(fetchChangesets(repo, branch, page));
}
};
};
const mapStateToProps = (state: any, ownProps: Props) => {
const { repository, branch, match } = ownProps;
const changesets = getChangesets(state, repository, branch);
const loading = isFetchChangesetsPending(state, repository, branch);
const error = getFetchChangesetsFailure(state, repository, branch);
const list = selectListAsCollection(state, repository, branch);
const page = getPageFromMatch(match);
return { changesets, list, page, loading, error };
};
export default compose(
withRouter,
connect(
mapStateToProps,
mapDispatchToProps
)
)(Changesets);

View File

@@ -8,14 +8,14 @@ import {
isFetchRepoPending isFetchRepoPending
} from "../modules/repos"; } from "../modules/repos";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Route, withRouter } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
import { import {
Page,
Loading,
ErrorPage, ErrorPage,
Loading,
Navigation, Navigation,
NavLink, NavLink,
Page,
Section Section
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
@@ -26,6 +26,7 @@ import Permissions from "../permissions/containers/Permissions";
import type { History } from "history"; import type { History } from "history";
import EditNavLink from "../components/EditNavLink"; import EditNavLink from "../components/EditNavLink";
import BranchRoot from "./BranchRoot";
import PermissionsNavLink from "../components/PermissionsNavLink"; import PermissionsNavLink from "../components/PermissionsNavLink";
import Sources from "../sources/containers/Sources"; import Sources from "../sources/containers/Sources";
import RepositoryNavLink from "../components/RepositoryNavLink"; import RepositoryNavLink from "../components/RepositoryNavLink";
@@ -73,6 +74,12 @@ class RepositoryRoot extends React.Component<Props> {
this.props.deleteRepo(repository, this.deleted); this.props.deleteRepo(repository, this.deleted);
}; };
matches = (route: any) => {
const url = this.matchedUrl();
const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`);
return route.location.pathname.match(regex);
};
render() { render() {
const { loading, error, repository, t } = this.props; const { loading, error, repository, t } = this.props;
@@ -91,60 +98,87 @@ class RepositoryRoot extends React.Component<Props> {
} }
const url = this.matchedUrl(); const url = this.matchedUrl();
return ( return (
<Page title={repository.namespace + "/" + repository.name}> <Page title={repository.namespace + "/" + repository.name}>
<div className="columns"> <div className="columns">
<div className="column is-three-quarters"> <div className="column is-three-quarters">
<Route <Switch>
path={url} <Route
exact path={url}
component={() => <RepositoryDetails repository={repository} />} exact
/> component={() => <RepositoryDetails repository={repository} />}
<Route />
path={`${url}/edit`} <Route
component={() => <Edit repository={repository} />} path={`${url}/edit`}
/> component={() => <Edit repository={repository} />}
<Route />
path={`${url}/permissions`} <Route
render={props => ( path={`${url}/permissions`}
<Permissions render={props => (
namespace={this.props.repository.namespace} <Permissions
repoName={this.props.repository.name} namespace={this.props.repository.namespace}
/> repoName={this.props.repository.name}
)} />
/> )}
<Route />
path={`${url}/sources`} <Route
exact={true} path={`${url}/sources`}
component={props => ( exact={true}
<Sources component={props => (
{...props} <Sources
repository={repository} {...props}
baseUrl={`${url}/sources`} repository={repository}
/> baseUrl={`${url}/sources`}
)} />
/> )}
<Route />
path={`${url}/sources/:revision/:path*`} <Route
component={props => ( path={`${url}/sources/:revision/:path*`}
<Sources component={props => (
{...props} <Sources
repository={repository} {...props}
baseUrl={`${url}/sources`} repository={repository}
/> baseUrl={`${url}/sources`}
)} />
/> )}
/>
<Route
path={`${url}/changesets`}
render={() => (
<BranchRoot
repository={repository}
baseUrlWithBranch={`${url}/branches`}
baseUrlWithoutBranch={`${url}/changesets`}
/>
)}
/>
<Route
path={`${url}/branches/:branch/changesets`}
render={() => (
<BranchRoot
repository={repository}
baseUrlWithBranch={`${url}/branches`}
baseUrlWithoutBranch={`${url}/changesets`}
/>
)}
/>
</Switch>
</div> </div>
<div className="column"> <div className="column">
<Navigation> <Navigation>
<Section label={t("repository-root.navigation-label")}> <Section label={t("repository-root.navigation-label")}>
<NavLink to={url} label={t("repository-root.information")} /> <NavLink to={url} label={t("repository-root.information")} />
<NavLink
activeOnlyWhenExact={false}
to={`${url}/changesets/`}
label={t("repository-root.history")}
activeWhenMatch={this.matches}
/>
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
<PermissionsNavLink <PermissionsNavLink
permissionUrl={`${url}/permissions`} permissionUrl={`${url}/permissions`}
repository={repository} repository={repository}
/> />
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
<RepositoryNavLink <RepositoryNavLink
repository={repository} repository={repository}
linkName="sources" linkName="sources"

View File

@@ -0,0 +1,134 @@
// @flow
import {
FAILURE_SUFFIX,
PENDING_SUFFIX,
SUCCESS_SUFFIX
} from "../../modules/types";
import { apiClient } from "@scm-manager/ui-components";
import type { Action, Branch, Repository } from "@scm-manager/ui-types";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES";
export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`;
export const FETCH_BRANCHES_SUCCESS = `${FETCH_BRANCHES}_${SUCCESS_SUFFIX}`;
export const FETCH_BRANCHES_FAILURE = `${FETCH_BRANCHES}_${FAILURE_SUFFIX}`;
// Fetching branches
export function fetchBranches(repository: Repository) {
if (!repository._links.branches) {
return {
type: FETCH_BRANCHES_SUCCESS,
payload: { repository, data: {} },
itemId: createKey(repository)
};
}
return function(dispatch: any) {
dispatch(fetchBranchesPending(repository));
return apiClient
.get(repository._links.branches.href)
.then(response => response.json())
.then(data => {
dispatch(fetchBranchesSuccess(data, repository));
})
.catch(error => {
dispatch(fetchBranchesFailure(repository, error));
});
};
}
// Action creators
export function fetchBranchesPending(repository: Repository) {
return {
type: FETCH_BRANCHES_PENDING,
payload: { repository },
itemId: createKey(repository)
};
}
export function fetchBranchesSuccess(data: string, repository: Repository) {
return {
type: FETCH_BRANCHES_SUCCESS,
payload: { data, repository },
itemId: createKey(repository)
};
}
export function fetchBranchesFailure(repository: Repository, error: Error) {
return {
type: FETCH_BRANCHES_FAILURE,
payload: { error, repository },
itemId: createKey(repository)
};
}
// Reducers
type State = { [string]: Branch[] };
export default function reducer(
state: State = {},
action: Action = { type: "UNKNOWN" }
): State {
if (!action.payload) {
return state;
}
const payload = action.payload;
switch (action.type) {
case FETCH_BRANCHES_SUCCESS:
const key = createKey(payload.repository);
return {
...state,
[key]: extractBranchesFromPayload(payload.data)
};
default:
return state;
}
}
function extractBranchesFromPayload(payload: any) {
if (payload._embedded && payload._embedded.branches) {
return payload._embedded.branches;
}
return [];
}
// Selectors
export function getBranches(state: Object, repository: Repository) {
const key = createKey(repository);
if (state.branches[key]) {
return state.branches[key];
}
return null;
}
export function getBranch(
state: Object,
repository: Repository,
name: string
): ?Branch {
const key = createKey(repository);
if (state.branches[key]) {
return state.branches[key].find((b: Branch) => b.name === name);
}
return null;
}
export function isFetchBranchesPending(
state: Object,
repository: Repository
): boolean {
return isPending(state, FETCH_BRANCHES, createKey(repository));
}
export function getFetchBranchesFailure(state: Object, repository: Repository) {
return getFailure(state, FETCH_BRANCHES, createKey(repository));
}
function createKey(repository: Repository): string {
const { namespace, name } = repository;
return `${namespace}/${name}`;
}

View File

@@ -0,0 +1,195 @@
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import reducer, {
FETCH_BRANCHES,
FETCH_BRANCHES_FAILURE,
FETCH_BRANCHES_PENDING,
FETCH_BRANCHES_SUCCESS,
fetchBranches,
getBranch,
getBranches,
getFetchBranchesFailure,
isFetchBranchesPending
} from "./branches";
const namespace = "foo";
const name = "bar";
const key = namespace + "/" + name;
const repository = {
namespace: "foo",
name: "bar",
_links: {
branches: {
href: "http://scm/api/rest/v2/repositories/foo/bar/branches"
}
}
};
const branch1 = { name: "branch1", revision: "revision1" };
const branch2 = { name: "branch2", revision: "revision2" };
const branch3 = { name: "branch3", revision: "revision3" };
describe("branches", () => {
describe("fetch branches", () => {
const URL = "http://scm/api/rest/v2/repositories/foo/bar/branches";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should fetch branches", () => {
const collection = {};
fetchMock.getOnce(URL, "{}");
const expectedActions = [
{
type: FETCH_BRANCHES_PENDING,
payload: { repository },
itemId: key
},
{
type: FETCH_BRANCHES_SUCCESS,
payload: { data: collection, repository },
itemId: key
}
];
const store = mockStore({});
return store.dispatch(fetchBranches(repository)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should fail fetching branches on HTTP 500", () => {
const collection = {};
fetchMock.getOnce(URL, 500);
const expectedActions = [
{
type: FETCH_BRANCHES_PENDING,
payload: { repository },
itemId: key
},
{
type: FETCH_BRANCHES_FAILURE,
payload: { error: collection, repository },
itemId: key
}
];
const store = mockStore({});
return store.dispatch(fetchBranches(repository)).then(() => {
expect(store.getActions()[0]).toEqual(expectedActions[0]);
expect(store.getActions()[1].type).toEqual(FETCH_BRANCHES_FAILURE);
});
});
});
describe("branches reducer", () => {
const branches = {
_embedded: {
branches: [branch1, branch2]
}
};
const action = {
type: FETCH_BRANCHES_SUCCESS,
payload: {
repository,
data: branches
}
};
it("should update state according to successful fetch", () => {
const newState = reducer({}, action);
expect(newState).toBeDefined();
expect(newState[key]).toBeDefined();
expect(newState[key]).toContain(branch1);
expect(newState[key]).toContain(branch2);
});
it("should not delete existing branches from state", () => {
const oldState = {
"hitchhiker/heartOfGold": [branch3]
};
const newState = reducer(oldState, action);
expect(newState[key]).toContain(branch1);
expect(newState[key]).toContain(branch2);
expect(newState["hitchhiker/heartOfGold"]).toContain(branch3);
});
});
describe("branch selectors", () => {
const error = new Error("Something went wrong");
const state = {
branches: {
[key]: [branch1, branch2]
}
};
it("should return true, when fetching branches is pending", () => {
const state = {
pending: {
[FETCH_BRANCHES + "/foo/bar"]: true
}
};
expect(isFetchBranchesPending(state, repository)).toBeTruthy();
});
it("should return branches", () => {
const branches = getBranches(state, repository);
expect(branches.length).toEqual(2);
expect(branches).toContain(branch1);
expect(branches).toContain(branch2);
});
it("should return always the same reference for branches", () => {
const one = getBranches(state, repository);
const two = getBranches(state, repository);
expect(one).toBe(two);
});
it("should return null, if no branches for the repository available", () => {
const branches = getBranches({ branches: {} }, repository);
expect(branches).toBeNull();
});
it("should return single branch by name", () => {
const branch = getBranch(state, repository, "branch1");
expect(branch).toEqual(branch1);
});
it("should return same reference for single branch by name", () => {
const one = getBranch(state, repository, "branch1");
const two = getBranch(state, repository, "branch1");
expect(one).toBe(two);
});
it("should return undefined if branch does not exist", () => {
const branch = getBranch(state, repository, "branch42");
expect(branch).toBeUndefined();
});
it("should return error if fetching branches failed", () => {
const state = {
failure: {
[FETCH_BRANCHES + "/foo/bar"]: error
}
};
expect(getFetchBranchesFailure(state, repository)).toEqual(error);
});
it("should return false if fetching branches did not fail", () => {
expect(getFetchBranchesFailure({}, repository)).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,230 @@
// @flow
import {
FAILURE_SUFFIX,
PENDING_SUFFIX,
SUCCESS_SUFFIX
} from "../../modules/types";
import { apiClient } from "@scm-manager/ui-components";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import type {
Action,
Branch,
PagedCollection,
Repository
} from "@scm-manager/ui-types";
export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS";
export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`;
export const FETCH_CHANGESETS_SUCCESS = `${FETCH_CHANGESETS}_${SUCCESS_SUFFIX}`;
export const FETCH_CHANGESETS_FAILURE = `${FETCH_CHANGESETS}_${FAILURE_SUFFIX}`;
//TODO: Content type
// actions
export function fetchChangesets(
repository: Repository,
branch?: Branch,
page?: number
) {
const link = createChangesetsLink(repository, branch, page);
return function(dispatch: any) {
dispatch(fetchChangesetsPending(repository, branch));
return apiClient
.get(link)
.then(response => response.json())
.then(data => {
dispatch(fetchChangesetsSuccess(repository, branch, data));
})
.catch(cause => {
dispatch(fetchChangesetsFailure(repository, branch, cause));
});
};
}
function createChangesetsLink(
repository: Repository,
branch?: Branch,
page?: number
) {
let link = repository._links.changesets.href;
if (branch) {
link = branch._links.history.href;
}
if (page) {
link = link + `?page=${page - 1}`;
}
return link;
}
export function fetchChangesetsPending(
repository: Repository,
branch?: Branch
): Action {
const itemId = createItemId(repository, branch);
return {
type: FETCH_CHANGESETS_PENDING,
itemId
};
}
export function fetchChangesetsSuccess(
repository: Repository,
branch?: Branch,
changesets: any
): Action {
return {
type: FETCH_CHANGESETS_SUCCESS,
payload: changesets,
itemId: createItemId(repository, branch)
};
}
function fetchChangesetsFailure(
repository: Repository,
branch?: Branch,
error: Error
): Action {
return {
type: FETCH_CHANGESETS_FAILURE,
payload: {
repository,
error,
branch
},
itemId: createItemId(repository, branch)
};
}
function createItemId(repository: Repository, branch?: Branch): string {
const { namespace, name } = repository;
let itemId = namespace + "/" + name;
if (branch) {
itemId = itemId + "/" + branch.name;
}
return itemId;
}
// reducer
export default function reducer(
state: any = {},
action: Action = { type: "UNKNOWN" }
): Object {
if (!action.payload) {
return state;
}
const payload = action.payload;
switch (action.type) {
case FETCH_CHANGESETS_SUCCESS:
const changesets = payload._embedded.changesets;
const changesetIds = changesets.map(c => c.id);
const key = action.itemId;
if (!key) {
return state;
}
let oldByIds = {};
if (state[key] && state[key].byId) {
oldByIds = state[key].byId;
}
const byIds = extractChangesetsByIds(changesets);
return {
...state,
[key]: {
byId: {
...oldByIds,
...byIds
},
list: {
entries: changesetIds,
entry: {
page: payload.page,
pageTotal: payload.pageTotal,
_links: payload._links
}
}
}
};
default:
return state;
}
}
function extractChangesetsByIds(changesets: any) {
const changesetsByIds = {};
for (let changeset of changesets) {
changesetsByIds[changeset.id] = changeset;
}
return changesetsByIds;
}
//selectors
export function getChangesets(
state: Object,
repository: Repository,
branch?: Branch
) {
const key = createItemId(repository, branch);
const changesets = state.changesets[key];
if (!changesets) {
return null;
}
return changesets.list.entries.map((id: string) => {
return changesets.byId[id];
});
}
export function isFetchChangesetsPending(
state: Object,
repository: Repository,
branch?: Branch
) {
return isPending(state, FETCH_CHANGESETS, createItemId(repository, branch));
}
export function getFetchChangesetsFailure(
state: Object,
repository: Repository,
branch?: Branch
) {
return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch));
}
const selectList = (state: Object, repository: Repository, branch?: Branch) => {
const itemId = createItemId(repository, branch);
if (state.changesets[itemId] && state.changesets[itemId].list) {
return state.changesets[itemId].list;
}
return {};
};
const selectListEntry = (
state: Object,
repository: Repository,
branch?: Branch
): Object => {
const list = selectList(state, repository, branch);
if (list.entry) {
return list.entry;
}
return {};
};
export const selectListAsCollection = (
state: Object,
repository: Repository,
branch?: Branch
): PagedCollection => {
return selectListEntry(state, repository, branch);
};

View File

@@ -0,0 +1,307 @@
// @flow
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import reducer, {
FETCH_CHANGESETS,
FETCH_CHANGESETS_FAILURE,
FETCH_CHANGESETS_PENDING,
FETCH_CHANGESETS_SUCCESS,
fetchChangesets,
fetchChangesetsSuccess,
getChangesets,
getFetchChangesetsFailure,
isFetchChangesetsPending
} from "./changesets";
const branch = {
name: "specific",
revision: "123",
_links: {
history: {
href:
"http://scm/api/rest/v2/repositories/foo/bar/branches/specific/changesets"
}
}
};
const repository = {
namespace: "foo",
name: "bar",
type: "GIT",
_links: {
self: {
href: "http://scm/api/rest/v2/repositories/foo/bar"
},
changesets: {
href: "http://scm/api/rest/v2/repositories/foo/bar/changesets"
},
branches: {
href:
"http://scm/api/rest/v2/repositories/foo/bar/branches/specific/branches"
}
}
};
const changesets = {};
describe("changesets", () => {
describe("fetching of changesets", () => {
const DEFAULT_BRANCH_URL =
"http://scm/api/rest/v2/repositories/foo/bar/changesets";
const SPECIFIC_BRANCH_URL =
"http://scm/api/rest/v2/repositories/foo/bar/branches/specific/changesets";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should fetch changesets for default branch", () => {
fetchMock.getOnce(DEFAULT_BRANCH_URL, "{}");
const expectedActions = [
{
type: FETCH_CHANGESETS_PENDING,
itemId: "foo/bar"
},
{
type: FETCH_CHANGESETS_SUCCESS,
payload: changesets,
itemId: "foo/bar"
}
];
const store = mockStore({});
return store.dispatch(fetchChangesets(repository)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should fetch changesets for specific branch", () => {
const itemId = "foo/bar/specific";
fetchMock.getOnce(SPECIFIC_BRANCH_URL, "{}");
const expectedActions = [
{
type: FETCH_CHANGESETS_PENDING,
itemId
},
{
type: FETCH_CHANGESETS_SUCCESS,
payload: changesets,
itemId
}
];
const store = mockStore({});
return store.dispatch(fetchChangesets(repository, branch)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should fail fetching changesets on error", () => {
const itemId = "foo/bar";
fetchMock.getOnce(DEFAULT_BRANCH_URL, 500);
const expectedActions = [
{
type: FETCH_CHANGESETS_PENDING,
itemId
}
];
const store = mockStore({});
return store.dispatch(fetchChangesets(repository)).then(() => {
expect(store.getActions()[0]).toEqual(expectedActions[0]);
expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE);
expect(store.getActions()[1].payload).toBeDefined();
});
});
it("should fail fetching changesets for specific branch on error", () => {
const itemId = "foo/bar/specific";
fetchMock.getOnce(SPECIFIC_BRANCH_URL, 500);
const expectedActions = [
{
type: FETCH_CHANGESETS_PENDING,
itemId
}
];
const store = mockStore({});
return store.dispatch(fetchChangesets(repository, branch)).then(() => {
expect(store.getActions()[0]).toEqual(expectedActions[0]);
expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE);
expect(store.getActions()[1].payload).toBeDefined();
});
});
it("should fetch changesets by page", () => {
fetchMock.getOnce(DEFAULT_BRANCH_URL + "?page=4", "{}");
const expectedActions = [
{
type: FETCH_CHANGESETS_PENDING,
itemId: "foo/bar"
},
{
type: FETCH_CHANGESETS_SUCCESS,
payload: changesets,
itemId: "foo/bar"
}
];
const store = mockStore({});
return store
.dispatch(fetchChangesets(repository, undefined, 5))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should fetch changesets by branch and page", () => {
fetchMock.getOnce(SPECIFIC_BRANCH_URL + "?page=4", "{}");
const expectedActions = [
{
type: FETCH_CHANGESETS_PENDING,
itemId: "foo/bar/specific"
},
{
type: FETCH_CHANGESETS_SUCCESS,
payload: changesets,
itemId: "foo/bar/specific"
}
];
const store = mockStore({});
return store.dispatch(fetchChangesets(repository, branch, 5)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
describe("changesets reducer", () => {
const responseBody = {
page: 1,
pageTotal: 10,
_links: {},
_embedded: {
changesets: [
{ id: "changeset1", author: { mail: "z@phod.com", name: "zaphod" } },
{ id: "changeset2", description: "foo" },
{ id: "changeset3", description: "bar" }
],
_embedded: {
tags: [],
branches: [],
parents: []
}
}
};
it("should set state to received changesets", () => {
const newState = reducer(
{},
fetchChangesetsSuccess(repository, undefined, responseBody)
);
expect(newState).toBeDefined();
expect(newState["foo/bar"].byId["changeset1"].author.mail).toEqual(
"z@phod.com"
);
expect(newState["foo/bar"].byId["changeset2"].description).toEqual("foo");
expect(newState["foo/bar"].byId["changeset3"].description).toEqual("bar");
expect(newState["foo/bar"].list).toEqual({
entry: {
page: 1,
pageTotal: 10,
_links: {}
},
entries: ["changeset1", "changeset2", "changeset3"]
});
});
it("should not remove existing changesets", () => {
const state = {
"foo/bar": {
byId: {
id2: { id: "id2" },
id1: { id: "id1" }
},
list: {
entries: ["id1", "id2"]
}
}
};
const newState = reducer(
state,
fetchChangesetsSuccess(repository, undefined, responseBody)
);
const fooBar = newState["foo/bar"];
expect(fooBar.list.entries).toEqual([
"changeset1",
"changeset2",
"changeset3"
]);
expect(fooBar.byId["id2"]).toEqual({ id: "id2" });
expect(fooBar.byId["id1"]).toEqual({ id: "id1" });
});
});
describe("changeset selectors", () => {
const error = new Error("Something went wrong");
it("should get all changesets for a given repository", () => {
const state = {
changesets: {
"foo/bar": {
byId: {
id2: { id: "id2" },
id1: { id: "id1" }
},
list: {
entries: ["id1", "id2"]
}
}
}
};
const result = getChangesets(state, repository);
expect(result).toEqual([{ id: "id1" }, { id: "id2" }]);
});
it("should return true, when fetching changesets is pending", () => {
const state = {
pending: {
[FETCH_CHANGESETS + "/foo/bar"]: true
}
};
expect(isFetchChangesetsPending(state, repository)).toBeTruthy();
});
it("should return false, when fetching changesets is not pending", () => {
expect(isFetchChangesetsPending({}, repository)).toEqual(false);
});
it("should return error if fetching changesets failed", () => {
const state = {
failure: {
[FETCH_CHANGESETS + "/foo/bar"]: error
}
};
expect(getFetchChangesetsFailure(state, repository)).toEqual(error);
});
it("should return false if fetching changesets did not fail", () => {
expect(getFetchChangesetsFailure({}, repository)).toBeUndefined();
});
});
});

View File

@@ -405,7 +405,7 @@ describe("repos fetch", () => {
}); });
}); });
it("should disapatch failure if server returns status code 500", () => { it("should dispatch failure if server returns status code 500", () => {
fetchMock.postOnce(REPOS_URL, { fetchMock.postOnce(REPOS_URL, {
status: 500 status: 500
}); });

View File

@@ -1,9 +1,7 @@
// @flow // @flow
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { import { Select } from "@scm-manager/ui-components";
Select
} from "@scm-manager/ui-components";
type Props = { type Props = {
t: string => string, t: string => string,
@@ -15,7 +13,7 @@ type Props = {
class TypeSelector extends React.Component<Props> { class TypeSelector extends React.Component<Props> {
render() { render() {
const { type, handleTypeChange, loading } = this.props; const { type, handleTypeChange, loading } = this.props;
const types = ["READ", "OWNER", "WRITE"]; const types = ["READ", "WRITE", "OWNER"];
return ( return (
<Select <Select

View File

@@ -5,12 +5,15 @@ import "../../../../tests/i18n";
import DeletePermissionButton from "./DeletePermissionButton"; import DeletePermissionButton from "./DeletePermissionButton";
import { confirmAlert } from "@scm-manager/ui-components"; import { confirmAlert } from "@scm-manager/ui-components";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
jest.mock("@scm-manager/ui-components", () => ({ jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(), confirmAlert: jest.fn(),
DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
})); }));
describe("DeletePermissionButton", () => { describe("DeletePermissionButton", () => {
const options = new ReactRouterEnzymeContext();
it("should render nothing, if the delete link is missing", () => { it("should render nothing, if the delete link is missing", () => {
const permission = { const permission = {
_links: {} _links: {}
@@ -20,7 +23,8 @@ describe("DeletePermissionButton", () => {
<DeletePermissionButton <DeletePermissionButton
permission={permission} permission={permission}
deletePermission={() => {}} deletePermission={() => {}}
/> />,
options.get()
); );
expect(navLink.text()).toBe(""); expect(navLink.text()).toBe("");
}); });
@@ -38,7 +42,8 @@ describe("DeletePermissionButton", () => {
<DeletePermissionButton <DeletePermissionButton
permission={permission} permission={permission}
deletePermission={() => {}} deletePermission={() => {}}
/> />,
options.get()
); );
expect(navLink.text()).not.toBe(""); expect(navLink.text()).not.toBe("");
}); });
@@ -56,7 +61,8 @@ describe("DeletePermissionButton", () => {
<DeletePermissionButton <DeletePermissionButton
permission={permission} permission={permission}
deletePermission={() => {}} deletePermission={() => {}}
/> />,
options.get()
); );
button.find("button").simulate("click"); button.find("button").simulate("click");
@@ -82,7 +88,8 @@ describe("DeletePermissionButton", () => {
permission={permission} permission={permission}
confirmDialog={false} confirmDialog={false}
deletePermission={capture} deletePermission={capture}
/> />,
options.get()
); );
button.find("button").simulate("click"); button.find("button").simulate("click");

View File

@@ -6573,7 +6573,7 @@ rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-dom@^16.4.2: react-dom@^16.4.2, react-dom@^16.5.2:
version "16.5.2" version "16.5.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
dependencies: dependencies:
@@ -6662,7 +6662,7 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.4.1:
react-is "^16.5.2" react-is "^16.5.2"
schedule "^0.5.0" schedule "^0.5.0"
react@^16.4.2: react@^16.4.2, react@^16.5.2:
version "16.5.2" version "16.5.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
dependencies: dependencies:

View File

@@ -109,9 +109,10 @@ public class BranchRootResource {
} }
Repository repository = repositoryService.getRepository(); Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check(); RepositoryPermissions.read(repository).check();
ChangesetPagingResult changesets = repositoryService.getLogCommand() ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService)
.setPagingStart(page) .page(page)
.setPagingLimit(pageSize) .pageSize(pageSize)
.create()
.setBranch(branchName) .setBranch(branchName)
.getChangesets(); .getChangesets();
if (changesets != null && changesets.getChangesets() != null) { if (changesets != null && changesets.getChangesets() != null) {

View File

@@ -12,6 +12,7 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.LogCommandBuilder;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
@@ -59,9 +60,10 @@ public class ChangesetRootResource {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Repository repository = repositoryService.getRepository(); Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check(); RepositoryPermissions.read(repository).check();
ChangesetPagingResult changesets = repositoryService.getLogCommand() ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService)
.setPagingStart(page) .page(page)
.setPagingLimit(pageSize) .pageSize(pageSize)
.create()
.getChangesets(); .getChangesets();
if (changesets != null && changesets.getChangesets() != null) { if (changesets != null && changesets.getChangesets() != null) {
PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); PageResult<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());

View File

@@ -73,9 +73,10 @@ public class FileHistoryRootResource {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
log.info("Get changesets of the file {} and revision {}", path, revision); log.info("Get changesets of the file {} and revision {}", path, revision);
Repository repository = repositoryService.getRepository(); Repository repository = repositoryService.getRepository();
ChangesetPagingResult changesets = repositoryService.getLogCommand() ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService)
.setPagingStart(page) .page(page)
.setPagingLimit(pageSize) .pageSize(pageSize)
.create()
.setPath(path) .setPath(path)
.setStartChangeset(revision) .setStartChangeset(revision)
.getChangesets(); .getChangesets();

View File

@@ -0,0 +1,30 @@
package sonia.scm.api.v2.resources;
import sonia.scm.repository.api.LogCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
class PagedLogCommandBuilder {
private final RepositoryService repositoryService;
private int page;
private int pageSize ;
PagedLogCommandBuilder(RepositoryService repositoryService) {
this.repositoryService = repositoryService;
}
PagedLogCommandBuilder page(int page) {
this.page = page;
return this;
}
PagedLogCommandBuilder pageSize(int pageSize) {
this.pageSize = pageSize;
return this;
}
LogCommandBuilder create() {
return repositoryService.getLogCommand()
.setPagingStart(page * pageSize)
.setPagingLimit(pageSize);
}
}

View File

@@ -34,24 +34,21 @@ package sonia.scm.filter;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.util.WebUtil; import sonia.scm.util.WebUtil;
import sonia.scm.web.filter.HttpFilter; import sonia.scm.web.filter.HttpFilter;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.FilterConfig; import javax.servlet.FilterConfig;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
@@ -109,11 +106,7 @@ public class StaticResourceFilter extends HttpFilter
{ {
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
StringBuilder msg = new StringBuilder("return "); logger.debug("return {} for {}" , HttpServletResponse.SC_NOT_MODIFIED, uri);
msg.append(HttpServletResponse.SC_NOT_MODIFIED);
msg.append(" for ").append(uri);
logger.debug(msg.toString());
} }
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);

View File

@@ -64,7 +64,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
private String subject; private String subject;
private String issuer; private String issuer;
private long expiresIn = 10l; private long expiresIn = 60l;
private TimeUnit expiresInUnit = TimeUnit.MINUTES; private TimeUnit expiresInUnit = TimeUnit.MINUTES;
private Scope scope = Scope.empty(); private Scope scope = Scope.empty();

View File

@@ -109,8 +109,8 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
when(changesetPagingResult.getChangesets()).thenReturn(changesetList); when(changesetPagingResult.getChangesets()).thenReturn(changesetList);
when(changesetPagingResult.getTotal()).thenReturn(1); when(changesetPagingResult.getTotal()).thenReturn(1);
when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); when(logCommandBuilder.setPagingStart(0)).thenReturn(logCommandBuilder);
when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); when(logCommandBuilder.setPagingLimit(10)).thenReturn(logCommandBuilder);
when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder); when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder);
when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
@@ -126,6 +126,34 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
} }
@Test
public void shouldGetSinglePageOfChangeSets() throws Exception {
String id = "revision_123";
Instant creationDate = Instant.now();
String authorName = "name";
String authorEmail = "em@i.l";
String commit = "my branch commit";
ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class);
List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
when(changesetPagingResult.getChangesets()).thenReturn(changesetList);
when(changesetPagingResult.getTotal()).thenReturn(1);
when(logCommandBuilder.setPagingStart(20)).thenReturn(logCommandBuilder);
when(logCommandBuilder.setPagingLimit(10)).thenReturn(logCommandBuilder);
when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder);
when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult);
MockHttpRequest request = MockHttpRequest
.get(CHANGESET_URL + "?page=2")
.accept(VndMediaType.CHANGESET_COLLECTION);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
log.info("Response :{}", response.getContentAsString());
assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id)));
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName)));
assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail)));
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
}
@Test @Test
public void shouldGetChangeSet() throws Exception { public void shouldGetChangeSet() throws Exception {
String id = "revision_123"; String id = "revision_123";
@@ -137,8 +165,6 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
when(changesetPagingResult.getChangesets()).thenReturn(changesetList); when(changesetPagingResult.getChangesets()).thenReturn(changesetList);
when(changesetPagingResult.getTotal()).thenReturn(1); when(changesetPagingResult.getTotal()).thenReturn(1);
when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder);
when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder);
when(logCommandBuilder.setEndChangeset(anyString())).thenReturn(logCommandBuilder); when(logCommandBuilder.setEndChangeset(anyString())).thenReturn(logCommandBuilder);
when(logCommandBuilder.setStartChangeset(anyString())).thenReturn(logCommandBuilder); when(logCommandBuilder.setStartChangeset(anyString())).thenReturn(logCommandBuilder);
when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult);