Merge pull request #1169 from scm-manager/feature/show-git-trailer

Show git trailer
This commit is contained in:
René Pfeuffer
2020-06-11 08:59:54 +02:00
committed by GitHub
38 changed files with 2233 additions and 103 deletions

View File

@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Option to configure jvm parameter of docker container with env JAVA_OPTS or with arguments ([#1175](https://github.com/scm-manager/scm-manager/pull/1175))
- Added links in diff views to expand the gaps between "hunks" ([#1178](https://github.com/scm-manager/scm-manager/pull/1178))
- Show commit contributors in table on changeset details view ([#1169](https://github.com/scm-manager/scm-manager/pull/1169))
### Fixed
- Avoid caching of detected browser language ([#1176](https://github.com/scm-manager/scm-manager/pull/1176))

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded;
@@ -32,6 +32,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
@@ -58,6 +59,8 @@ public class ChangesetDto extends HalRepresentation {
*/
private String description;
private List<ContributorDto> contributors;
public ChangesetDto(Links links, Embedded embedded) {
super(links, embedded);
}

View File

@@ -0,0 +1,37 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class ContributorDto {
private String type;
private PersonDto person;
}

View File

@@ -31,6 +31,7 @@ import sonia.scm.util.Util;
import sonia.scm.util.ValidationUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@@ -79,6 +80,11 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
*/
private List<String> tags;
/**
* Trailers for this changeset like reviewers or co-authors
*/
private Collection<Contributor> contributors;
public Changeset() {}
public Changeset(String id, Long date, Person author)
@@ -225,6 +231,15 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
return tags;
}
/**
* Returns collection of contributors for this changeset.
* @return collection of contributors
* @since 2.1.0
*/
public Collection<Contributor> getContributors() {
return contributors;
}
/**
* Returns true if the changeset is valid.
*
@@ -300,4 +315,37 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
this.tags = tags;
}
/**
* Sets the collection of contributors.
* @param contributors collection of contributors
* @since 2.1.0
*/
public void setContributors(Collection<Contributor> contributors) {
this.contributors = new ArrayList<>(contributors);
}
/**
* Adds a contributor to the list of contributors.
* @param contributor contributor to add
* @since 2.1.0
*/
public void addContributor(Contributor contributor) {
if (contributors == null) {
contributors = new ArrayList<>();
}
contributors.add(contributor);
}
/**
* Adds all contributors from the given collection to the list of contributors.
* @param contributors collection of contributor
* @since 2.1.0
*/
public void addContributors(Collection<Contributor> contributors) {
if (this.contributors == null) {
this.contributors = new ArrayList<>(contributors);
} else {
this.contributors.addAll(contributors);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import lombok.Value;
import java.io.Serializable;
@Value
public class Contributor implements Serializable {
private String type;
private Person person;
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
@@ -171,8 +171,8 @@ public class GitChangesetConverter implements Closeable
long date = GitUtil.getCommitTime(commit);
PersonIdent authorIndent = commit.getAuthorIdent();
Person author = new Person(authorIndent.getName(),
authorIndent.getEmailAddress());
PersonIdent committerIdent = commit.getCommitterIdent();
Person author = createPersonFor(authorIndent);
String message = commit.getFullMessage();
if (message != null)
@@ -181,6 +181,9 @@ public class GitChangesetConverter implements Closeable
}
Changeset changeset = new Changeset(id, date, author, message);
if (!committerIdent.equals(authorIndent)) {
changeset.addContributor(new Contributor("Committed-by", createPersonFor(committerIdent)));
}
if (parentList != null)
{
@@ -201,6 +204,9 @@ public class GitChangesetConverter implements Closeable
return changeset;
}
public Person createPersonFor(PersonIdent personIndent) {
return new Person(personIndent.getName(), personIndent.getEmailAddress());
}
//~--- fields ---------------------------------------------------------------

View File

@@ -25,7 +25,6 @@
package sonia.scm.repository.spi;
import com.google.common.io.Files;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -34,6 +33,7 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Person;
import java.io.File;
import java.io.IOException;
@@ -271,6 +271,20 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("master", changesets.getBranchName());
}
@Test
public void shouldAppendCommitterAsContributor() {
LogCommandRequest request = new LogCommandRequest();
request.setStartChangeset("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
request.setEndChangeset("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
ChangesetPagingResult changesets = createCommand().getChangesets(request);
Changeset changeset = changesets.getChangesets().get(0);
assertThat(changeset.getContributors()).hasSize(1);
assertThat(changeset.getContributors().iterator().next().getPerson())
.isEqualTo(new Person("Sebastian Sdorra", "s.sdorra@ostfalia.de"));
}
private void setRepositoryHeadReference(String s) throws IOException {
Files.write(s, repositoryHeadReferenceFile(), defaultCharset());
}

View File

@@ -0,0 +1,51 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
const CommaSeparatedList: FC = ({ children }) => {
const [t] = useTranslation("commons");
const childArray = React.Children.toArray(children);
return (
<>
{childArray.map((p, i) => {
if (i === 0) {
return <React.Fragment key={i}>{p}</React.Fragment>;
} else if (i + 1 === childArray.length) {
return (
<React.Fragment key={i}>
{" "}
{t("commaSeparatedList.lastDivider")} {p}{" "}
</React.Fragment>
);
} else {
return <React.Fragment key={i}>, {p}</React.Fragment>;
}
})}
</>
);
};
export default CommaSeparatedList;

View File

@@ -27,6 +27,7 @@ import { withContextPath } from "./urls";
type Props = {
src: string;
alt: string;
title?: string;
className?: string;
};
@@ -40,8 +41,8 @@ class Image extends React.Component<Props> {
};
render() {
const { alt, className } = this.props;
return <img className={className} src={this.createImageSrc()} alt={alt} />;
const { alt, title, className } = this.props;
return <img className={className} src={this.createImageSrc()} alt={alt} title={title} />;
}
}

View File

@@ -0,0 +1,250 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { Changeset, PagedCollection } from "@scm-manager/ui-types";
const one: Changeset = {
id: "a88567ef1e9528a700555cad8c4576b72fc7c6dd",
author: { mail: "scm-admin@scm-manager.org", name: "SCM Administrator" },
date: new Date("2020-06-09T06:34:47Z"),
description:
"The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive. The craft was stolen by then-President Zaphod Beeblebrox at the official launch of the ship, as he was supposed to be officiating the launch. Later, during the use of the Infinite Improbability Drive, the ship picked up Arthur Dent and Ford Prefect, who were floating unprotected in deep space in the same star sector, having just escaped the destruction of the same planet.\n\n",
contributors: [
{
type: "Committed-by",
person: { mail: "zaphod.beeblebrox@hitchhiker.cm", name: "Zaphod Beeblebrox" }
},
{ type: "Co-authored-by", person: { mail: "ford.prefect@hitchhiker.com", name: "Ford Prefect" } }
],
_links: {
self: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
},
diff: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
},
sources: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
},
modifications: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/modifications/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
},
diffParsed: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/a88567ef1e9528a700555cad8c4576b72fc7c6dd/parsed"
}
},
_embedded: {
tags: [],
branches: [],
parents: [
{
id: "d21cc6c359270aef2196796f4d96af65f51866dc",
_links: {
self: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/d21cc6c359270aef2196796f4d96af65f51866dc"
},
diff: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/d21cc6c359270aef2196796f4d96af65f51866dc"
}
}
}
]
}
};
const two: Changeset = {
id: "d21cc6c359270aef2196796f4d96af65f51866dc",
author: { mail: "scm-admin@scm-manager.org", name: "SCM Administrator" },
date: new Date("2020-06-09T05:39:50Z"),
description: 'Change heading to "Heart Of Gold"\n\n',
contributors: [
{
type: "Committed-by",
person: { mail: "zaphod.beeblebrox@hitchhiker.cm", name: "Zaphod Beeblebrox" }
}
],
_links: {
self: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/d21cc6c359270aef2196796f4d96af65f51866dc"
},
diff: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/d21cc6c359270aef2196796f4d96af65f51866dc"
},
sources: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
},
modifications: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/modifications/d21cc6c359270aef2196796f4d96af65f51866dc"
},
diffParsed: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/d21cc6c359270aef2196796f4d96af65f51866dc/parsed"
}
},
_embedded: {
tags: [],
branches: [],
parents: [
{
id: "e163c8f632db571c9aa51a8eb440e37cf550b825",
_links: {
self: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/e163c8f632db571c9aa51a8eb440e37cf550b825"
},
diff: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/e163c8f632db571c9aa51a8eb440e37cf550b825"
}
}
}
]
}
};
const three: Changeset = {
id: "e163c8f632db571c9aa51a8eb440e37cf550b825",
author: { mail: "scm-admin@scm-manager.org", name: "SCM Administrator" },
date: new Date("2020-06-09T05:25:16Z"),
description: "initialize repository",
contributors: [],
_links: {
self: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/e163c8f632db571c9aa51a8eb440e37cf550b825"
},
diff: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/e163c8f632db571c9aa51a8eb440e37cf550b825"
},
sources: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
},
modifications: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/modifications/e163c8f632db571c9aa51a8eb440e37cf550b825"
},
diffParsed: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/e163c8f632db571c9aa51a8eb440e37cf550b825/parsed"
}
},
_embedded: { tags: [], branches: [], parents: [] }
};
const four: Changeset = {
id: "b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930",
author: { mail: "scm-admin@scm-manager.org", name: "SCM Administrator" },
date: new Date("2020-06-09T09:23:49Z"),
description: "Added design docs\n\n",
contributors: [
{ type: "Co-authored-by", person: { mail: "ford.prefect@hitchhiker.com", name: "Ford Prefect" } },
{ type: "Co-authored-by", person: { mail: "zaphod.beeblebrox@hitchhiker.cm", name: "Zaphod Beeblebrox" } },
{ type: "Co-authored-by", person: { mail: "trillian@hitchhiker.cm", name: "Tricia Marie McMillan" } }
],
_links: {
self: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
},
diff: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
},
sources: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/sources/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
},
modifications: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/modifications/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
},
diffParsed: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930/parsed"
}
},
_embedded: {
tags: [],
branches: [],
parents: [
{
id: "a88567ef1e9528a700555cad8c4576b72fc7c6dd",
_links: {
self: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
},
diff: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
}
}
}
]
}
};
const changesets: PagedCollection = {
page: 0,
pageTotal: 1,
_links: {
self: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/branches/master/changesets/?page=0&pageSize=10"
},
first: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/branches/master/changesets/?page=0&pageSize=10"
},
last: {
href:
"http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/branches/master/changesets/?page=0&pageSize=10"
}
},
_embedded: {
changesets: [one, two, three, four],
branch: {
name: "master",
_links: {
self: { href: "http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/branches/master" }
}
}
}
};
export { one, two, three, four };
export default changesets;

View File

@@ -831,6 +831,937 @@ exports[`Storyshots CardColumnSmall Minimal 1`] = `
</div>
`;
exports[`Storyshots Changesets Co-Authors with avatar 1`] = `
<div
className="Changesetsstories__Wrapper-sc-122npan-0 iszpMD box box-link-shadow"
>
<div
className="ChangesetRow__Wrapper-tkpti5-0 iLSrdy"
>
<div
className="columns is-gapless is-mobile"
>
<div
className="column is-three-fifths"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<figure
className="ChangesetRow__AvatarFigure-tkpti5-1 cqLirB media-left"
>
<div
className="ChangesetRow__FixedSizedAvatar-tkpti5-2 fqLFVn image"
>
<img
alt="SCM Administrator"
className="has-rounded-border"
src="https://robohash.org/scm-admin@scm-manager.org"
/>
</div>
</figure>
<div
className="ChangesetRow__Metadata-tkpti5-3 ibQmQZ media-right"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
Added design docs
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<p
className="ChangesetRow__AuthorWrapper-tkpti5-4 cVHztK is-size-7 is-ellipsis-overflow"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
commaSeparatedList.lastDivider
changeset.contributors.coAuthoredBy
<span
className="ChangesetAuthor__AvatarList-sc-1oz0xgw-0 glabbT"
>
<a
href="mailto:ford.prefect@hitchhiker.com"
title="changeset.contributors.mailto ford.prefect@hitchhiker.com"
>
<img
alt="Ford Prefect"
className="ContributorAvatar-sc-1yz8zn-0 lgGcHZ"
src="https://robohash.org/ford.prefect@hitchhiker.com"
/>
</a>
<a
href="mailto:zaphod.beeblebrox@hitchhiker.cm"
title="changeset.contributors.mailto zaphod.beeblebrox@hitchhiker.cm"
>
<img
alt="Zaphod Beeblebrox"
className="ContributorAvatar-sc-1yz8zn-0 lgGcHZ"
src="https://robohash.org/zaphod.beeblebrox@hitchhiker.cm"
/>
</a>
<a
href="mailto:trillian@hitchhiker.cm"
title="changeset.contributors.mailto trillian@hitchhiker.cm"
>
<img
alt="Tricia Marie McMillan"
className="ContributorAvatar-sc-1yz8zn-0 lgGcHZ"
src="https://robohash.org/trillian@hitchhiker.cm"
/>
</a>
</span>
</p>
</div>
</div>
</div>
<div
className="ChangesetRow__VCenteredColumn-tkpti5-5 idGTGx column"
/>
</div>
</div>
<div
className="ChangesetRow__VCenteredChildColumn-tkpti5-6 gmuCVd column is-flex"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 cicrPZ field has-addons is-marginless"
>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-exchange-alt has-text-inherit"
/>
</span>
<span>
changeset.buttons.details
</span>
</button>
</p>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-code has-text-inherit"
/>
</span>
<span>
changeset.buttons.sources
</span>
</button>
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots Changesets Commiter and Co-Authors with avatar 1`] = `
<div
className="Changesetsstories__Wrapper-sc-122npan-0 iszpMD box box-link-shadow"
>
<div
className="ChangesetRow__Wrapper-tkpti5-0 iLSrdy"
>
<div
className="columns is-gapless is-mobile"
>
<div
className="column is-three-fifths"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<figure
className="ChangesetRow__AvatarFigure-tkpti5-1 cqLirB media-left"
>
<div
className="ChangesetRow__FixedSizedAvatar-tkpti5-2 fqLFVn image"
>
<img
alt="SCM Administrator"
className="has-rounded-border"
src="https://robohash.org/scm-admin@scm-manager.org"
/>
</div>
</figure>
<div
className="ChangesetRow__Metadata-tkpti5-3 ibQmQZ media-right"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive. The craft was stolen by then-President Zaphod Beeblebrox at the official launch of the ship, as he was supposed to be officiating the launch. Later, during the use of the Infinite Improbability Drive, the ship picked up Arthur Dent and Ford Prefect, who were floating unprotected in deep space in the same star sector, having just escaped the destruction of the same planet.
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<p
className="ChangesetRow__AuthorWrapper-tkpti5-4 cVHztK is-size-7 is-ellipsis-overflow"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
,
changeset.contributors.committedBy
<a
href="mailto:zaphod.beeblebrox@hitchhiker.cm"
title="changeset.contributors.mailto zaphod.beeblebrox@hitchhiker.cm"
>
<img
alt="Zaphod Beeblebrox"
className="ContributorAvatar-sc-1yz8zn-0 lgGcHZ"
src="https://robohash.org/zaphod.beeblebrox@hitchhiker.cm"
/>
</a>
commaSeparatedList.lastDivider
changeset.contributors.coAuthoredBy
<a
href="mailto:ford.prefect@hitchhiker.com"
title="changeset.contributors.mailto ford.prefect@hitchhiker.com"
>
<img
alt="Ford Prefect"
className="ContributorAvatar-sc-1yz8zn-0 lgGcHZ"
src="https://robohash.org/ford.prefect@hitchhiker.com"
/>
</a>
</p>
</div>
</div>
</div>
<div
className="ChangesetRow__VCenteredColumn-tkpti5-5 idGTGx column"
/>
</div>
</div>
<div
className="ChangesetRow__VCenteredChildColumn-tkpti5-6 gmuCVd column is-flex"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 cicrPZ field has-addons is-marginless"
>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-exchange-alt has-text-inherit"
/>
</span>
<span>
changeset.buttons.details
</span>
</button>
</p>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-code has-text-inherit"
/>
</span>
<span>
changeset.buttons.sources
</span>
</button>
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots Changesets Default 1`] = `
<div
className="Changesetsstories__Wrapper-sc-122npan-0 iszpMD box box-link-shadow"
>
<div
className="ChangesetRow__Wrapper-tkpti5-0 iLSrdy"
>
<div
className="columns is-gapless is-mobile"
>
<div
className="column is-three-fifths"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<div
className="ChangesetRow__Metadata-tkpti5-3 ibQmQZ media-right"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
initialize repository
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<p
className="ChangesetRow__AuthorWrapper-tkpti5-4 cVHztK is-size-7 is-ellipsis-overflow"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
</p>
</div>
</div>
</div>
<div
className="ChangesetRow__VCenteredColumn-tkpti5-5 idGTGx column"
/>
</div>
</div>
<div
className="ChangesetRow__VCenteredChildColumn-tkpti5-6 gmuCVd column is-flex"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 cicrPZ field has-addons is-marginless"
>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-exchange-alt has-text-inherit"
/>
</span>
<span>
changeset.buttons.details
</span>
</button>
</p>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-code has-text-inherit"
/>
</span>
<span>
changeset.buttons.sources
</span>
</button>
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots Changesets With Committer 1`] = `
<div
className="Changesetsstories__Wrapper-sc-122npan-0 iszpMD box box-link-shadow"
>
<div
className="ChangesetRow__Wrapper-tkpti5-0 iLSrdy"
>
<div
className="columns is-gapless is-mobile"
>
<div
className="column is-three-fifths"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<div
className="ChangesetRow__Metadata-tkpti5-3 ibQmQZ media-right"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
Change heading to "Heart Of Gold"
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<p
className="ChangesetRow__AuthorWrapper-tkpti5-4 cVHztK is-size-7 is-ellipsis-overflow"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
commaSeparatedList.lastDivider
changeset.contributors.committedBy
<a
href="mailto:zaphod.beeblebrox@hitchhiker.cm"
title="changeset.contributors.mailto zaphod.beeblebrox@hitchhiker.cm"
>
Zaphod Beeblebrox
</a>
</p>
</div>
</div>
</div>
<div
className="ChangesetRow__VCenteredColumn-tkpti5-5 idGTGx column"
/>
</div>
</div>
<div
className="ChangesetRow__VCenteredChildColumn-tkpti5-6 gmuCVd column is-flex"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 cicrPZ field has-addons is-marginless"
>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-exchange-alt has-text-inherit"
/>
</span>
<span>
changeset.buttons.details
</span>
</button>
</p>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-code has-text-inherit"
/>
</span>
<span>
changeset.buttons.sources
</span>
</button>
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots Changesets With Committer and Co-Author 1`] = `
<div
className="Changesetsstories__Wrapper-sc-122npan-0 iszpMD box box-link-shadow"
>
<div
className="ChangesetRow__Wrapper-tkpti5-0 iLSrdy"
>
<div
className="columns is-gapless is-mobile"
>
<div
className="column is-three-fifths"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<div
className="ChangesetRow__Metadata-tkpti5-3 ibQmQZ media-right"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive. The craft was stolen by then-President Zaphod Beeblebrox at the official launch of the ship, as he was supposed to be officiating the launch. Later, during the use of the Infinite Improbability Drive, the ship picked up Arthur Dent and Ford Prefect, who were floating unprotected in deep space in the same star sector, having just escaped the destruction of the same planet.
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<p
className="ChangesetRow__AuthorWrapper-tkpti5-4 cVHztK is-size-7 is-ellipsis-overflow"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
,
changeset.contributors.committedBy
<a
href="mailto:zaphod.beeblebrox@hitchhiker.cm"
title="changeset.contributors.mailto zaphod.beeblebrox@hitchhiker.cm"
>
Zaphod Beeblebrox
</a>
commaSeparatedList.lastDivider
changeset.contributors.coAuthoredBy
<a
href="mailto:ford.prefect@hitchhiker.com"
title="changeset.contributors.mailto ford.prefect@hitchhiker.com"
>
Ford Prefect
</a>
</p>
</div>
</div>
</div>
<div
className="ChangesetRow__VCenteredColumn-tkpti5-5 idGTGx column"
/>
</div>
</div>
<div
className="ChangesetRow__VCenteredChildColumn-tkpti5-6 gmuCVd column is-flex"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 cicrPZ field has-addons is-marginless"
>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-exchange-alt has-text-inherit"
/>
</span>
<span>
changeset.buttons.details
</span>
</button>
</p>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-code has-text-inherit"
/>
</span>
<span>
changeset.buttons.sources
</span>
</button>
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots Changesets With avatar 1`] = `
<div
className="Changesetsstories__Wrapper-sc-122npan-0 iszpMD box box-link-shadow"
>
<div
className="ChangesetRow__Wrapper-tkpti5-0 iLSrdy"
>
<div
className="columns is-gapless is-mobile"
>
<div
className="column is-three-fifths"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<figure
className="ChangesetRow__AvatarFigure-tkpti5-1 cqLirB media-left"
>
<div
className="ChangesetRow__FixedSizedAvatar-tkpti5-2 fqLFVn image"
>
<img
alt="SCM Administrator"
className="has-rounded-border"
src="test-file-stub"
/>
</div>
</figure>
<div
className="ChangesetRow__Metadata-tkpti5-3 ibQmQZ media-right"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
initialize repository
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<p
className="ChangesetRow__AuthorWrapper-tkpti5-4 cVHztK is-size-7 is-ellipsis-overflow"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
</p>
</div>
</div>
</div>
<div
className="ChangesetRow__VCenteredColumn-tkpti5-5 idGTGx column"
/>
</div>
</div>
<div
className="ChangesetRow__VCenteredChildColumn-tkpti5-6 gmuCVd column is-flex"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 cicrPZ field has-addons is-marginless"
>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-exchange-alt has-text-inherit"
/>
</span>
<span>
changeset.buttons.details
</span>
</button>
</p>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-code has-text-inherit"
/>
</span>
<span>
changeset.buttons.sources
</span>
</button>
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots Changesets With multiple Co-Authors 1`] = `
<div
className="Changesetsstories__Wrapper-sc-122npan-0 iszpMD box box-link-shadow"
>
<div
className="ChangesetRow__Wrapper-tkpti5-0 iLSrdy"
>
<div
className="columns is-gapless is-mobile"
>
<div
className="column is-three-fifths"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<div
className="ChangesetRow__Metadata-tkpti5-3 ibQmQZ media-right"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
Added design docs
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<p
className="ChangesetRow__AuthorWrapper-tkpti5-4 cVHztK is-size-7 is-ellipsis-overflow"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
commaSeparatedList.lastDivider
changeset.contributors.coAuthoredBy
<a
title="- Ford Prefect
- Zaphod Beeblebrox
- Tricia Marie McMillan"
>
changeset.contributors.more
</a>
</p>
</div>
</div>
</div>
<div
className="ChangesetRow__VCenteredColumn-tkpti5-5 idGTGx column"
/>
</div>
</div>
<div
className="ChangesetRow__VCenteredChildColumn-tkpti5-6 gmuCVd column is-flex"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 cicrPZ field has-addons is-marginless"
>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-exchange-alt has-text-inherit"
/>
</span>
<span>
changeset.buttons.details
</span>
</button>
</p>
<p
className="control"
>
<button
className="button is-default is-reduced-mobile"
onClick={[Function]}
type="button"
>
<span
className="icon is-medium"
>
<i
className="fas fa-code has-text-inherit"
/>
</span>
<span>
changeset.buttons.sources
</span>
</button>
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots DateFromNow Default 1`] = `
<div>
<p>

View File

@@ -22,9 +22,10 @@
* SOFTWARE.
*/
export type Person = {
name: string;
mail?: string;
};
import { Person } from "@scm-manager/ui-types";
// re export type to avoid breaking changes,
// after the type was moved to ui-types
export { Person };
export const EXTENSION_POINT = "avatar.factory";

View File

@@ -76,6 +76,7 @@ export { default as OverviewPageActions } from "./OverviewPageActions";
export { default as CardColumnGroup } from "./CardColumnGroup";
export { default as CardColumn } from "./CardColumn";
export { default as CardColumnSmall } from "./CardColumnSmall";
export { default as CommaSeparatedList } from "./CommaSeparatedList";
export { default as comparators } from "./comparators";

View File

@@ -21,53 +21,157 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { Changeset } from "@scm-manager/ui-types";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC } from "react";
import { Changeset, Person } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next";
import { useBinder } from "@scm-manager/ui-extensions";
import { EXTENSION_POINT } from "../../avatar/Avatar";
import styled from "styled-components";
import CommaSeparatedList from "../../CommaSeparatedList";
import ContributorAvatar from "./ContributorAvatar";
type Props = WithTranslation & {
type Props = {
changeset: Changeset;
};
class ChangesetAuthor extends React.Component<Props> {
render() {
const { changeset } = this.props;
if (!changeset.author) {
return null;
}
type PersonProps = {
person: Person;
displayTextOnly?: boolean;
};
const { name, mail } = changeset.author;
if (mail) {
return this.withExtensionPoint(this.renderWithMail(name, mail));
}
return this.withExtensionPoint(<>{name}</>);
const useAvatar = (person: Person): string | undefined => {
const binder = useBinder();
const factory: (person: Person) => string | undefined = binder.getExtension(EXTENSION_POINT);
if (factory) {
return factory(person);
}
};
renderWithMail(name: string, mail: string) {
const { t } = this.props;
const AvatarList = styled.span`
& > :not(:last-child) {
margin-right: 0.25em;
}
`;
type PersonAvatarProps = {
person: Person;
avatar: string;
};
const ContributorWithAvatar: FC<PersonAvatarProps> = ({ person, avatar }) => {
const [t] = useTranslation("repos");
if (person.mail) {
return (
<a href={"mailto:" + mail} title={t("changeset.author.mailto") + " " + mail}>
{name}
<a href={"mailto:" + person.mail} title={t("changeset.contributors.mailto") + " " + person.mail}>
<ContributorAvatar src={avatar} alt={person.name} />
</a>
);
}
return <ContributorAvatar src={avatar} alt={person.name} title={person.name} />;
};
withExtensionPoint(child: any) {
const { t } = this.props;
const SingleContributor: FC<PersonProps> = ({ person, displayTextOnly }) => {
const [t] = useTranslation("repos");
const avatar = useAvatar(person);
if (!displayTextOnly && avatar) {
return <ContributorWithAvatar person={person} avatar={avatar} />;
}
if (person.mail) {
return (
<a href={"mailto:" + person.mail} title={t("changeset.contributors.mailto") + " " + person.mail}>
{person.name}
</a>
);
}
return <>{person.name}</>;
};
type PersonsProps = {
persons: Person[];
label: string;
displayTextOnly?: boolean;
};
const Contributors: FC<PersonsProps> = ({ persons, label, displayTextOnly }) => {
const binder = useBinder();
const [t] = useTranslation("repos");
if (persons.length === 1) {
return (
<>
{t("changeset.author.prefix")} {child}
<ExtensionPoint
name="changesets.author.suffix"
props={{
changeset: this.props.changeset
}}
renderAll={true}
/>
{t(label)} <SingleContributor person={persons[0]} displayTextOnly={displayTextOnly} />
</>
);
}
}
export default withTranslation("repos")(ChangesetAuthor);
const avatarFactory = binder.getExtension(EXTENSION_POINT);
if (avatarFactory) {
return (
<>
{t(label)}{" "}
<AvatarList>
{persons.map(p => (
<ContributorWithAvatar key={p.name} person={p} avatar={avatarFactory(p)} />
))}
</AvatarList>
</>
);
} else {
return (
<>
{t(label)}{" "}
<a title={persons.map(person => "- " + person.name).join("\n")}>
{t("changeset.contributors.more", { count: persons.length })}
</a>
</>
);
}
};
const emptyListOfContributors: Person[] = [];
const ChangesetAuthor: FC<Props> = ({ changeset }) => {
const binder = useBinder();
const getCoAuthors = () => {
return filterContributorsByType("Co-authored-by");
};
const getCommitters = () => {
return filterContributorsByType("Committed-by");
};
const filterContributorsByType = (type: string) => {
if (changeset.contributors) {
return changeset.contributors.filter(p => p.type === type).map(contributor => contributor.person);
}
return emptyListOfContributors;
};
const authorLine = [];
if (changeset.author) {
authorLine.push(
<Contributors persons={[changeset.author]} label={"changeset.contributors.authoredBy"} displayTextOnly={true} />
);
}
const commiters = getCommitters();
if (commiters.length > 0) {
authorLine.push(<Contributors persons={commiters} label={"changeset.contributors.committedBy"} />);
}
const coAuthors = getCoAuthors();
if (coAuthors.length > 0) {
authorLine.push(<Contributors persons={coAuthors} label={"changeset.contributors.coAuthoredBy"} />);
}
// extensions
const extensions = binder.getExtensions("changesets.author.suffix", { changeset });
if (extensions) {
coAuthors.push(...extensions);
}
return <CommaSeparatedList>{authorLine}</CommaSeparatedList>;
};
export default ChangesetAuthor;

View File

@@ -123,7 +123,7 @@ class ChangesetRow extends React.Component<Props> {
<p className="is-hidden-desktop">
<Trans i18nKey="repos:changeset.shortSummary" components={[changesetId, dateFromNow]} />
</p>
<AuthorWrapper className="is-size-7">
<AuthorWrapper className="is-size-7 is-ellipsis-overflow">
<ChangesetAuthor changeset={changeset} />
</AuthorWrapper>
</Metadata>

View File

@@ -0,0 +1,79 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { storiesOf } from "@storybook/react";
import * as React from "react";
import styled from "styled-components";
import { MemoryRouter } from "react-router-dom";
import repository from "../../__resources__/repository";
import ChangesetRow from "./ChangesetRow";
import {one, two, three, four} from "../../__resources__/changesets";
import {Binder, BinderContext} from "@scm-manager/ui-extensions";
// @ts-ignore
import hitchhiker from "../../__resources__/hitchhiker.png";
import {Person} from "../../avatar/Avatar";
import {Changeset} from "@scm-manager/ui-types/src";
const Wrapper = styled.div`
margin: 2rem;
`;
const robohash = (person: Person) => {
return `https://robohash.org/${person.mail}`;
}
const withAvatarFactory = (factory: (person: Person) => string, changeset: Changeset) => {
const binder = new Binder("changeset stories");
binder.bind("avatar.factory", factory);
return (
<BinderContext.Provider value={binder}>
<ChangesetRow repository={repository} changeset={changeset} />
</BinderContext.Provider>
);
};
storiesOf("Changesets", module)
.addDecorator(story => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
.addDecorator(storyFn => <Wrapper className="box box-link-shadow">{storyFn()}</Wrapper>)
.add("Default", () => (
<ChangesetRow repository={repository} changeset={three} />
))
.add("With Committer", () => (
<ChangesetRow repository={repository} changeset={two} />
))
.add("With Committer and Co-Author", () => (
<ChangesetRow repository={repository} changeset={one} />
))
.add("With multiple Co-Authors", () => (
<ChangesetRow repository={repository} changeset={four} />
))
.add("With avatar", () => {
return withAvatarFactory(person => hitchhiker, three);
})
.add("Commiter and Co-Authors with avatar", () => {
return withAvatarFactory(robohash, one);
})
.add("Co-Authors with avatar", () => {
return withAvatarFactory(robohash, four);
});

View File

@@ -0,0 +1,36 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import styled from "styled-components";
import Image from "../../Image";
const ContributorAvatar = styled(Image)`
width: 1em;
height: 1em;
vertical-align: middle;
border-radius: 0.25em;
margin-bottom: 0.2em;
`;
export default ContributorAvatar;

View File

@@ -34,3 +34,4 @@ export { default as ChangesetRow } from "./ChangesetRow";
export { default as ChangesetTag } from "./ChangesetTag";
export { default as ChangesetTags } from "./ChangesetTags";
export { default as ChangesetTagsCollapsed } from "./ChangesetTagsCollapsed";
export { default as ContributorAvatar } from "./ContributorAvatar";

View File

@@ -26,14 +26,17 @@ import { Collection, Links } from "./hal";
import { Tag } from "./Tags";
import { Branch } from "./Branches";
export type Person = {
name: string;
mail?: string;
};
export type Changeset = Collection & {
id: string;
date: Date;
author: {
name: string;
mail?: string;
};
author: Person;
description: string;
contributors?: Contributor[];
_links: Links;
_embedded: {
tags?: Tag[];
@@ -42,6 +45,11 @@ export type Changeset = Collection & {
};
};
export type Contributor = {
person: Person;
type: string;
};
export type ParentChangeset = {
id: string;
_links: Links;

View File

@@ -29,16 +29,12 @@ export { Me } from "./Me";
export { DisplayedUser, User } from "./User";
export { Group, Member } from "./Group";
export {
Repository,
RepositoryCollection,
RepositoryGroup
} from "./Repositories";
export { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
export { Branch, BranchRequest } from "./Branches";
export { Changeset } from "./Changesets";
export { Changeset, Person, Contributor, ParentChangeset } from "./Changesets";
export { Tag } from "./Tags";
@@ -46,22 +42,13 @@ export { Config } from "./Config";
export { IndexResources } from "./IndexResources";
export {
Permission,
PermissionCreateEntry,
PermissionCollection
} from "./RepositoryPermissions";
export { Permission, PermissionCreateEntry, PermissionCollection } from "./RepositoryPermissions";
export { SubRepository, File } from "./Sources";
export { SelectValue, AutocompleteObject } from "./Autocomplete";
export {
Plugin,
PluginCollection,
PluginGroup,
PendingPlugins
} from "./Plugin";
export { Plugin, PluginCollection, PluginGroup, PendingPlugins } from "./Plugin";
export { RepositoryRole } from "./RepositoryRole";

View File

@@ -104,5 +104,8 @@
"community": "Community",
"enterprise": "Enterprise"
}
},
"commaSeparatedList": {
"lastDivider": "und"
}
}

View File

@@ -87,9 +87,15 @@
"shortSummary": "Committet <0/> <1/>",
"tags": "Tags",
"diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt",
"author": {
"prefix": "Verfasst von",
"mailto": "Mail senden an"
"contributors": {
"mailto": "Mail senden an",
"list": "Liste der Mitwirkenden",
"authoredBy": "Verfasst von",
"committedBy": "Committed von",
"coAuthoredBy": "Co-Autoren",
"more": "{{count}} mehr",
"count": "{{count}} Mitwirkender",
"count_plural": "{{count}} Mitwirkende"
},
"buttons": {
"details": "Details",

View File

@@ -105,5 +105,8 @@
"community": "Community",
"enterprise": "Enterprise"
}
},
"commaSeparatedList": {
"lastDivider": "and"
}
}

View File

@@ -87,13 +87,19 @@
"shortSummary": "Committed <0/> <1/>",
"tags": "Tags",
"diffNotSupported": "Diff of changesets is not supported by the type of repository",
"author": {
"prefix": "Authored by",
"mailto": "Send mail to"
},
"buttons": {
"details": "Details",
"sources": "Sources"
},
"contributors": {
"mailto": "Send mail to",
"list": "List of contributors",
"authoredBy": "Authored by",
"committedBy": "committed by",
"coAuthoredBy": "co authored by",
"more": "{{count}} more",
"count": "{{count}} Contributor",
"count_plural": "{{count}} Contributors"
}
},
"repositoryForm": {

View File

@@ -89,5 +89,8 @@
"passwordConfirmFailed": "Las contraseñas deben ser identicas",
"submit": "Guardar",
"changedSuccessfully": "Contraseña cambiada correctamente"
},
"commaSeparatedList": {
"lastDivider": "y"
}
}

View File

@@ -79,7 +79,8 @@
"errorSubtitle": "No se han podido recuperar los changesets",
"noChangesets": "No se han encontrado changesets para esta rama branch. Los commits podrían haber sido eliminados.",
"branchSelectorLabel": "Ramas",
"collapseDiffs": "Colapso"
"collapseDiffs": "Colapso",
"contributors": "Lista de contribuyentes"
},
"changeset": {
"description": "Descripción",

View File

@@ -21,8 +21,8 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { Trans, WithTranslation, withTranslation } from "react-i18next";
import React, { FC, useState } from "react";
import { Trans, useTranslation, WithTranslation, withTranslation } from "react-i18next";
import classNames from "classnames";
import styled from "styled-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
@@ -37,8 +37,10 @@ import {
changesets,
ChangesetTag,
DateFromNow,
Level
Level,
Icon
} from "@scm-manager/ui-components";
import ContributorTable from "./ContributorTable";
type Props = WithTranslation & {
changeset: Changeset;
@@ -63,6 +65,74 @@ const BottomMarginLevel = styled(Level)`
margin-bottom: 1rem !important;
`;
const countContributors = (changeset: Changeset) => {
if (changeset.contributors) {
return changeset.contributors.length + 1;
}
return 1;
};
const ContributorLine = styled.div`
display: flex;
cursor: pointer;
`;
const ContributorColumn = styled.p`
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
`;
const CountColumn = styled.p`
text-align: right;
white-space: nowrap;
`;
const ContributorDetails = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 1rem;
`;
const ContributorToggleLine = styled.p`
cursor: pointer;
/** maring-bottom is inherit from content p **/
margin-bottom: 0.5rem !important;
`;
const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
const [t] = useTranslation("repos");
const [open, setOpen] = useState(false);
if (open) {
return (
<ContributorDetails>
<ContributorToggleLine onClick={e => setOpen(!open)}>
<Icon name="angle-down" /> {t("changeset.contributors.list")}
</ContributorToggleLine>
<ContributorTable changeset={changeset} />
</ContributorDetails>
);
}
return (
<>
<ContributorLine onClick={e => setOpen(!open)}>
<ContributorColumn>
<Icon name="angle-right" /> <ChangesetAuthor changeset={changeset} />
</ContributorColumn>
<CountColumn>
(
<span className="has-text-link">
{t("changeset.contributors.count", { count: countContributors(changeset) })}
</span>
)
</CountColumn>
</ContributorLine>
</>
);
};
class ChangesetDetails extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
@@ -100,17 +170,14 @@ class ChangesetDetails extends React.Component<Props, State> {
<AvatarImage person={changeset.author} />
</RightMarginP>
</AvatarWrapper>
<div className="media-content">
<p>
<ChangesetAuthor changeset={changeset} />
</p>
<div className="media-content is-ellipsis-overflow">
<Contributors changeset={changeset} />
<p>
<Trans i18nKey="repos:changeset.summary" components={[id, date]} />
</p>
</div>
<div className="media-right">{this.renderTags()}</div>
</article>
<p>
{description.message.split("\n").map((item, key) => {
return (

View File

@@ -0,0 +1,114 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { Changeset, Person } from "@scm-manager/ui-types";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { useBinder } from "@scm-manager/ui-extensions";
import { ContributorAvatar, CommaSeparatedList } from "@scm-manager/ui-components";
type Props = {
changeset: Changeset;
};
const SizedTd = styled.td`
width: 10rem;
`;
const Contributor: FC<{ person: Person }> = ({ person }) => {
const [t] = useTranslation("repos");
const binder = useBinder();
const avatarFactory = binder.getExtension("avatar.factory");
let prefix = null;
if (avatarFactory) {
const avatar = avatarFactory(person);
if (avatar) {
prefix = (
<>
<ContributorAvatar src={avatar} alt={person.name} />{" "}
</>
);
}
}
if (person.mail) {
return (
<a href={"mailto:" + person.mail} title={t("changeset.contributors.mailto") + " " + person.mail}>
{prefix}
{person.name}
</a>
);
}
return <>{person.name}</>;
};
const ContributorTable: FC<Props> = ({ changeset }) => {
const [t] = useTranslation("plugins");
const collectAvailableContributorTypes = () => {
if (!changeset.contributors) {
return [];
}
// @ts-ignore
return [...new Set(changeset.contributors.map(contributor => contributor.type))];
};
const getPersonsByContributorType = (type: string) => {
return changeset.contributors?.filter(contributor => contributor.type === type).map(t => t.person);
};
const getContributorsByType = () => {
const availableContributorTypes: string[] = collectAvailableContributorTypes();
const personsByContributorType = [];
for (const type of availableContributorTypes) {
personsByContributorType.push({ type, persons: getPersonsByContributorType(type) });
}
return personsByContributorType;
};
return (
<table>
<tr>
<SizedTd>{t("changeset.contributor.type.author")}:</SizedTd>
<td>
<Contributor person={changeset.author} />
</td>
</tr>
{getContributorsByType().map(contributor => (
<tr key={contributor.type}>
<SizedTd>{t("changeset.contributor.type." + contributor.type)}:</SizedTd>
<td className="is-ellipsis-overflow is-marginless">
<CommaSeparatedList>
{contributor.persons!.map(person => (
<Contributor key={person.name} person={person} />
))}
</CommaSeparatedList>
</td>
</tr>
))}
</table>
);
};
export default ContributorTable;

View File

@@ -21,19 +21,20 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links;
import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ObjectFactory;
import sonia.scm.repository.Branch;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Person;
import sonia.scm.repository.Repository;
import sonia.scm.repository.Tag;
import sonia.scm.repository.Contributor;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
@@ -49,7 +50,7 @@ import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMapper implements InstantAttributeMapper, ChangesetToChangesetDtoMapper{
public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMapper implements InstantAttributeMapper, ChangesetToChangesetDtoMapper {
@Inject
private RepositoryServiceFactory serviceFactory;
@@ -66,10 +67,9 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
@Inject
private TagCollectionToDtoMapper tagCollectionToDtoMapper;
abstract ContributorDto map(Contributor contributor);
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract ChangesetDto map(Changeset changeset, @Context Repository repository);
abstract PersonDto map(Person person);
@ObjectFactory
ChangesetDto createDto(@Context Repository repository, Changeset source) {

View File

@@ -111,7 +111,6 @@ import sonia.scm.web.security.DefaultAdministrationContext;
import javax.net.ssl.SSLContext;
/**
*
* @author Sebastian Sdorra
*/
class ScmServletModule extends ServletModule {

View File

@@ -0,0 +1,114 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import com.google.common.collect.ImmutableSet;
import sonia.scm.plugin.Extension;
import java.util.Collection;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Extension
public class ChangesetDescriptionContributorProvider implements ChangesetPreProcessorFactory {
private static final Collection<String> SUPPORTED_CONTRIBUTOR_TYPES = ImmutableSet.of("Co-authored-by", "Reviewed-by", "Signed-off-by", "Committed-by");
private static final Pattern CONTRIBUTOR_PATTERN = Pattern.compile("^([\\w-]*):\\W*(.*)\\W+<(.*)>\\W*$");
@Override
public ChangesetPreProcessor createPreProcessor(Repository repository) {
return new ContributorChangesetPreProcessor();
}
private static class ContributorChangesetPreProcessor implements ChangesetPreProcessor {
@Override
public void process(Changeset changeset) {
new Worker(changeset).process();
}
}
private static class Worker {
private final StringBuilder newDescription = new StringBuilder();
private final Changeset changeset;
boolean foundEmptyLine;
private Worker(Changeset changeset) {
this.changeset = changeset;
}
private void process() {
try (Scanner scanner = new Scanner(changeset.getDescription())) {
while (scanner.hasNextLine()) {
handleLine(scanner, scanner.nextLine());
}
}
changeset.setDescription(newDescription.toString());
}
private void handleLine(Scanner scanner, String line) {
if (line.trim().isEmpty()) {
handleEmptyLine(scanner, line);
return;
}
if (foundEmptyLine && checkForContributor(line)) {
return;
}
appendLine(scanner, line);
}
private boolean checkForContributor(String line) {
Matcher matcher = CONTRIBUTOR_PATTERN.matcher(line);
if (matcher.matches()) {
String type = matcher.group(1);
String name = matcher.group(2);
String mail = matcher.group(3);
if (SUPPORTED_CONTRIBUTOR_TYPES.contains(type)) {
createContributor(type, name, mail);
return true;
}
}
return false;
}
private void handleEmptyLine(Scanner scanner, String line) {
foundEmptyLine = true;
appendLine(scanner, line);
}
private void appendLine(Scanner scanner, String line) {
newDescription.append(line);
if (scanner.hasNextLine()) {
newDescription.append('\n');
}
}
private void createContributor(String type, String name, String mail) {
changeset.addContributor(new Contributor(type, new Person(name, mail)));
}
}
}

View File

@@ -1,4 +1,15 @@
{
"changeset": {
"contributor": {
"type": {
"author": "Autor",
"Reviewed-by": "Reviewer",
"Co-authored-by": "Co-Autoren",
"Committed-by": "Committed von",
"Signed-off-by": "Signiert von"
}
}
},
"permissions": {
"*": {
"displayName": "Globaler Administrator",

View File

@@ -1,4 +1,15 @@
{
"changeset": {
"contributor": {
"type": {
"author": "Author",
"Reviewed-by": "Reviewers",
"Co-authored-by": "Co-Authors",
"Committed-by": "Commit by",
"Signed-off-by": "Signed-off"
}
}
},
"permissions": {
"*": {
"displayName": "Global administrator",

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.google.inject.util.Providers;
@@ -30,7 +30,6 @@ 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.assertj.core.api.Assertions;
import org.assertj.core.util.Lists;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
@@ -61,7 +60,9 @@ import java.net.URI;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -113,7 +114,6 @@ public class BranchRootResourceTest extends RepositoryTestBase {
@Mock
private TagCollectionToDtoMapper tagCollectionToDtoMapper;
@InjectMocks
private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper;
@@ -156,7 +156,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
assertEquals(404, response.getStatus());
MediaType contentType = (MediaType) response.getOutputHeaders().getFirst("Content-Type");
Assertions.assertThat(response.getContentAsString()).contains("branch", "master", "space/repo");
assertThat(response.getContentAsString()).contains("branch", "master", "space/repo");
}
@Test
@@ -196,10 +196,10 @@ public class BranchRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
log.info("Response :{}", response.getContentAsString());
assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", REVISION)));
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)));
assertThat(response.getContentAsString()).contains(String.format("\"id\":\"%s\"", REVISION));
assertThat(response.getContentAsString()).contains(String.format("\"name\":\"%s\"", authorName));
assertThat(response.getContentAsString()).contains(String.format("\"mail\":\"%s\"", authorEmail));
assertThat(response.getContentAsString()).contains(String.format("\"description\":\"%s\"", commit));
}
@Test

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
@@ -68,7 +68,6 @@ import static org.mockito.Mockito.when;
@Slf4j
public class ChangesetRootResourceTest extends RepositoryTestBase {
public static final String CHANGESET_PATH = "space/repo/changesets/";
public static final String CHANGESET_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + CHANGESET_PATH;
@@ -86,6 +85,7 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
@Mock
private LogCommandBuilder logCommandBuilder;
@InjectMocks
private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
@InjectMocks
@@ -93,11 +93,9 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
private ChangesetRootResource changesetRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@Before
public void prepareEnvironment() {
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.google.inject.util.Providers;

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
@@ -78,7 +78,6 @@ import static sonia.scm.repository.api.DiffFormat.NATIVE;
@Slf4j
public class IncomingRootResourceTest extends RepositoryTestBase {
public static final String INCOMING_PATH = "space/repo/incoming/";
public static final String INCOMING_CHANGESETS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
public static final String INCOMING_DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;

View File

@@ -0,0 +1,201 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collection;
import java.util.Iterator;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class ChangesetDescriptionContributorProviderTest {
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
private final ChangesetDescriptionContributorProvider changesetDescriptionContributors = new ChangesetDescriptionContributorProvider();
@Test
void shouldReturnEmptyList() {
Changeset changeset = createChangeset("zaphod beeblebrox");
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
assertThat(contributors).isNullOrEmpty();
assertThat(changeset.getDescription()).isEqualTo("zaphod beeblebrox");
}
@Test
void shouldConvertTrailerWithCoAuthors() {
Person person = createPerson("Arthur Dent", "dent@hitchhiker.org");
Changeset changeset = createChangeset("zaphod beeblebrox\n\nCo-authored-by: Arthur Dent <dent@hitchhiker.org>");
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
Contributor contributor = contributors.iterator().next();
assertThat(contributor.getType()).isEqualTo("Co-authored-by");
assertThat(contributor.getPerson()).isEqualTo(person);
assertThat(changeset.getDescription()).isEqualTo("zaphod beeblebrox\n\n");
}
@Test
void shouldConvertTrailerWithReviewers() {
Person person = createPerson("Tricia McMillan", "trillian@hitchhiker.org");
Changeset changeset = createChangeset("zaphod beeblebrox\n\nReviewed-by: Tricia McMillan <trillian@hitchhiker.org>");
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
Contributor contributor = contributors.iterator().next();
assertThat(contributor.getType()).isEqualTo("Reviewed-by");
assertThat(contributor.getPerson()).isEqualTo(person);
assertThat(changeset.getDescription()).isEqualTo("zaphod beeblebrox\n\n");
}
@Test
void shouldConvertTrailerWithSigner() {
Person person = createPerson("Tricia McMillan", "trillian@hitchhiker.org");
Changeset changeset = createChangeset("zaphod beeblebrox\n\n\nSigned-off-by: Tricia McMillan <trillian@hitchhiker.org>");
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
Contributor contributor = contributors.iterator().next();
assertThat(contributor.getType()).isEqualTo("Signed-off-by");
assertThat(contributor.getPerson()).isEqualTo(person);
assertThat(changeset.getDescription()).isEqualTo("zaphod beeblebrox\n\n\n");
}
@Test
void shouldConvertTrailerWithCommitter() {
Person person = createPerson("Tricia McMillan", "trillian@hitchhiker.org");
Changeset changeset = createChangeset("zaphod beeblebrox\n\nCommitted-by: Tricia McMillan <trillian@hitchhiker.org>");
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
Contributor contributor = contributors.iterator().next();
assertThat(contributor.getType()).isEqualTo("Committed-by");
assertThat(contributor.getPerson()).isEqualTo(person);
assertThat(changeset.getDescription()).isEqualTo("zaphod beeblebrox\n\n");
}
@Test
void shouldConvertMixedTrailers() {
Changeset changeset = createChangeset("zaphod beeblebrox\n\n" +
"Committed-by: Tricia McMillan <trillian@hitchhiker.org>\n" +
"Signed-off-by: Artur Dent <dent@hitchhiker.org>");
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
Iterator<Contributor> contributorIterator = contributors.iterator();
Contributor firstContributor = contributorIterator.next();
Contributor secondContributor = contributorIterator.next();
assertThat(firstContributor.getType()).isEqualTo("Committed-by");
assertThat(firstContributor.getPerson())
.isEqualTo(createPerson("Tricia McMillan", "trillian@hitchhiker.org"));
assertThat(secondContributor.getType()).isEqualTo("Signed-off-by");
assertThat(secondContributor.getPerson())
.isEqualTo(createPerson("Artur Dent", "dent@hitchhiker.org"));
assertThat(changeset.getDescription()).isEqualTo("zaphod beeblebrox\n\n");
}
@Test
void shouldNotTouchUnknownTrailers() {
String originalCommitMessage = "zaphod beeblebrox\n\n" +
"Some-strange-tag: Tricia McMillan <trillian@hitchhiker.org>";
Changeset changeset = createChangeset(originalCommitMessage);
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
assertThat(contributors).isNullOrEmpty();
assertThat(changeset.getDescription()).isEqualTo(originalCommitMessage);
}
@Test
void shouldIgnoreKnownTrailersWithIllegalNameFormat() {
String originalCommitMessage = "zaphod beeblebrox\n\n" +
"Committed-by: Tricia McMillan";
Changeset changeset = createChangeset(originalCommitMessage);
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
assertThat(contributors).isNullOrEmpty();
assertThat(changeset.getDescription()).isEqualTo(originalCommitMessage);
}
@Test
void shouldIgnoreWhitespacesInEmptyLines() {
String originalCommitMessage = "zaphod beeblebrox\n \n" +
"Committed-by: Tricia McMillan <trillian@hitchhiker.org>";
Changeset changeset = createChangeset(originalCommitMessage);
changesetDescriptionContributors.createPreProcessor(REPOSITORY).process(changeset);
Collection<Contributor> contributors = changeset.getContributors();
assertThat(contributors).isNotEmpty();
}
@Test
void shouldProcessChangesetsSeparately() {
Changeset changeset1 = createChangeset("message one\n\n" +
"Committed-by: Tricia McMillan <trillian@hitchhiker.org>");
Changeset changeset2 = createChangeset("message two");
ChangesetPreProcessor preProcessor = changesetDescriptionContributors.createPreProcessor(REPOSITORY);
preProcessor.process(changeset1);
preProcessor.process(changeset2);
assertThat(changeset1.getDescription()).isEqualTo("message one\n\n");
assertThat(changeset1.getContributors()).isNotEmpty();
assertThat(changeset2.getDescription()).isEqualTo("message two");
assertThat(changeset2.getContributors()).isNullOrEmpty();
}
private Changeset createChangeset(String commitMessage) {
Changeset changeset = new Changeset();
changeset.setDescription(commitMessage);
return changeset;
}
private Person createPerson(String name, String mail) {
return new Person(name, mail);
}
}