From bb1126befcc86e4c26d66400d53f97780cfcbd51 Mon Sep 17 00:00:00 2001 From: Andrzej Polit Date: Wed, 27 May 2020 13:49:19 +0200 Subject: [PATCH] Added ChangesetTrailerExtractor --- .../scm/api/v2/resources/ChangesetDto.java | 3 + .../scm/repository/ChangesetTrailerTypes.java | 35 +++++ .../resources/ChangesetTrailerExtractor.java | 92 ++++++++++++ .../DefaultChangesetToChangesetDtoMapper.java | 11 +- .../ChangesetTrailerExtractorTest.java | 142 ++++++++++++++++++ 5 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/ChangesetTrailerTypes.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetTrailerExtractor.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetTrailerExtractorTest.java diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java index 95d02eba28..801593be6e 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java @@ -32,6 +32,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import java.time.Instant; +import java.util.Map; @Getter @Setter @@ -58,6 +59,8 @@ public class ChangesetDto extends HalRepresentation { */ private String description; + private Map trailerPersons; + public ChangesetDto(Links links, Embedded embedded) { super(links, embedded); } diff --git a/scm-core/src/main/java/sonia/scm/repository/ChangesetTrailerTypes.java b/scm-core/src/main/java/sonia/scm/repository/ChangesetTrailerTypes.java new file mode 100644 index 0000000000..7d5c5c89d6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/ChangesetTrailerTypes.java @@ -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 sonia.scm.plugin.ExtensionPoint; + +import java.util.List; + +@ExtensionPoint +public interface ChangesetTrailerTypes { + + List getTrailerTypes(); +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetTrailerExtractor.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetTrailerExtractor.java new file mode 100644 index 0000000000..b5661c27db --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetTrailerExtractor.java @@ -0,0 +1,92 @@ +/* + * 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 com.google.common.collect.ImmutableList; +import sonia.scm.repository.ChangesetTrailerTypes; +import sonia.scm.user.DisplayUser; +import sonia.scm.user.UserDisplayManager; + +import javax.inject.Inject; +import java.util.*; +import java.util.regex.Pattern; + +public class ChangesetTrailerExtractor { + + private final UserDisplayManager userDisplayManager; + private final ChangesetTrailerTypes changesetTrailerTypes; + + @Inject + public ChangesetTrailerExtractor(UserDisplayManager userDisplayManager, ChangesetTrailerTypes changesetTrailerTypes) { + this.userDisplayManager = userDisplayManager; + this.changesetTrailerTypes = changesetTrailerTypes; + } + + Map extractTrailersFromCommitMessage(String commitMessage) { + + HashMap persons = new HashMap<>(); + + try (Scanner scanner = new Scanner(commitMessage)) { + scanner.useDelimiter(Pattern.compile("[\\n;]")); + while (scanner.hasNext()) { + String line = scanner.next(); + + for (String trailerType : changesetTrailerTypes.getTrailerTypes()) { + if (line.contains(trailerType)) { + String mail = line.split("<|>")[1]; + persons.put(trailerType, createPersonDtoFromUser(mail)); + } + } + +/* if (line.contains("Co-authored-by")) { + persons.put("Co-authored-by", createPersonDtoFromUser(mail)); + } + if (line.contains("Reviewed-by")) { + persons.put("Reviewed-by", createPersonDtoFromUser(mail)); + }*/ + + } + } + + + return persons; + } + + private PersonDto createPersonDtoFromUser(String mail) { + DisplayUser displayUser = userDisplayManager.autocomplete(mail).iterator().next(); + PersonDto personDto = new PersonDto(); + personDto.setName(displayUser.getDisplayName()); + personDto.setMail(displayUser.getMail()); + return personDto; + } + + static class TrailerTypes implements ChangesetTrailerTypes { + + @Override + public List getTrailerTypes() { + return ImmutableList.of("Co-authored-by", "Reviewed-by", "Signed-off-by", "Committed-by"); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java index a9ba0758a6..d68e32dc6e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java @@ -26,10 +26,7 @@ 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 org.mapstruct.*; import sonia.scm.repository.Branch; import sonia.scm.repository.Changeset; import sonia.scm.repository.Repository; @@ -66,10 +63,16 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa @Inject private TagCollectionToDtoMapper tagCollectionToDtoMapper; + @Inject + private ChangesetTrailerExtractor changesetTrailerExtractor; @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract ChangesetDto map(Changeset changeset, @Context Repository repository); + @AfterMapping + void appendTrailerPersons(Changeset changeset, @MappingTarget ChangesetDto target) { + target.setTrailerPersons(changesetTrailerExtractor.extractTrailersFromCommitMessage(changeset.getDescription())); + } @ObjectFactory ChangesetDto createDto(@Context Repository repository, Changeset source) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetTrailerExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetTrailerExtractorTest.java new file mode 100644 index 0000000000..58657cab8f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetTrailerExtractorTest.java @@ -0,0 +1,142 @@ +/* + * 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 com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.api.v2.resources.ChangesetTrailerExtractor.TrailerTypes; +import sonia.scm.user.DisplayUser; +import sonia.scm.user.User; +import sonia.scm.user.UserDisplayManager; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ChangesetTrailerExtractorTest { + + @Mock + private UserDisplayManager userDisplayManager; + + private ChangesetTrailerExtractor extractor; + + @BeforeEach + void initExtractor() { + TrailerTypes trailerTypes = new TrailerTypes(); + extractor = new ChangesetTrailerExtractor(userDisplayManager, trailerTypes); + } + + @Test + void shouldReturnEmptyMap() { + String commitMessage = "zaphod beetlebrox"; + + Map map = extractor.extractTrailersFromCommitMessage(commitMessage); + + assertThat(map).isInstanceOf(Map.class); + assertThat(map).isNotNull(); + assertThat(map.keySet()).isEmpty(); + } + + @Test + void shouldReturnMapWithCoAuthors() { + DisplayUser displayUser = mockDisplayUser("Arthur Dent", "dent@hitchhiker.org"); + String commitMessage = "zaphod beetlebrox\n\nCo-authored-by: Arthur Dent "; + + PersonDto personDto = createPersonDto(displayUser); + + Map map = extractor.extractTrailersFromCommitMessage(commitMessage); + + assertThat(map.keySet().iterator().next()).isEqualTo("Co-authored-by"); + PersonDto person = map.values().iterator().next(); + assertThat(person.getName()).isEqualTo(personDto.getName()); + assertThat(person.getMail()).isEqualTo(personDto.getMail()); + } + + @Test + void shouldReturnMapWithReviewers() { + DisplayUser displayUser = mockDisplayUser("Tricia McMillan", "trillian@hitchhiker.org"); + String commitMessage = "zaphod beetlebrox\nReviewed-by: Tricia McMillan "; + + PersonDto personDto = createPersonDto(displayUser); + + Map map = extractor.extractTrailersFromCommitMessage(commitMessage); + + assertThat(map.keySet().iterator().next()).isEqualTo("Reviewed-by"); + PersonDto person = map.values().iterator().next(); + assertThat(person.getName()).isEqualTo(personDto.getName()); + assertThat(person.getMail()).isEqualTo(personDto.getMail()); + } + + @Test + void shouldReturnMapWithSigner() { + DisplayUser displayUser = mockDisplayUser("Tricia McMillan", "trillian@hitchhiker.org"); + String commitMessage = "zaphod beetlebrox\nSigned-off-by: Tricia McMillan "; + + PersonDto personDto = createPersonDto(displayUser); + + Map map = extractor.extractTrailersFromCommitMessage(commitMessage); + + assertThat(map.keySet().iterator().next()).isEqualTo("Signed-off-by"); + PersonDto person = map.values().iterator().next(); + assertThat(person.getName()).isEqualTo(personDto.getName()); + assertThat(person.getMail()).isEqualTo(personDto.getMail()); + } + + @Test + void shouldReturnMapWithCommitter() { + DisplayUser displayUser = mockDisplayUser("Tricia McMillan", "trillian@hitchhiker.org"); + String commitMessage = "zaphod beetlebrox\nCommitted-by: Tricia McMillan "; + + PersonDto personDto = createPersonDto(displayUser); + + Map map = extractor.extractTrailersFromCommitMessage(commitMessage); + + assertThat(map.keySet().iterator().next()).isEqualTo("Committed-by"); + PersonDto person = map.values().iterator().next(); + assertThat(person.getName()).isEqualTo(personDto.getName()); + assertThat(person.getMail()).isEqualTo(personDto.getMail()); + } + + + private DisplayUser mockDisplayUser(String name, String mail) { + DisplayUser displayUser = DisplayUser.from(new User(name, name, mail)); + when(userDisplayManager.autocomplete(mail)).thenReturn(ImmutableList.of(displayUser)); + return displayUser; + } + + private PersonDto createPersonDto(DisplayUser displayUser) { + PersonDto personDto = new PersonDto(); + personDto.setName(displayUser.getDisplayName()); + personDto.setMail(displayUser.getMail()); + return personDto; + } + +}