From 0b30c4f94b23decd9b0d4b3d28f2ab8560ec4903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 11 Jun 2020 14:56:18 +0200 Subject: [PATCH 01/26] Encapsulate dependencies for repository resource --- .../RepositoryBasedResourceProvider.java | 105 ++++++++++++++++++ .../api/v2/resources/RepositoryResource.java | 55 +++------ .../v2/resources/BranchRootResourceTest.java | 3 - .../resources/ChangesetRootResourceTest.java | 3 - .../api/v2/resources/DiffResourceTest.java | 6 +- .../v2/resources/FileHistoryResourceTest.java | 3 - .../resources/IncomingRootResourceTest.java | 4 - .../resources/ModificationsResourceTest.java | 4 - .../RepositoryPermissionRootResourceTest.java | 7 +- .../resources/RepositoryRootResourceTest.java | 4 +- .../api/v2/resources/RepositoryTestBase.java | 73 ++++++------ .../v2/resources/SourceRootResourceTest.java | 5 +- .../api/v2/resources/TagRootResourceTest.java | 6 +- 13 files changed, 165 insertions(+), 113 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java new file mode 100644 index 0000000000..96580a717c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java @@ -0,0 +1,105 @@ +/* + * 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 javax.inject.Inject; +import javax.inject.Provider; + +public class RepositoryBasedResourceProvider { + private final Provider tagRootResource; + private final Provider branchRootResource; + private final Provider changesetRootResource; + private final Provider sourceRootResource; + private final Provider contentResource; + private final Provider permissionRootResource; + private final Provider diffRootResource; + private final Provider modificationsRootResource; + private final Provider fileHistoryRootResource; + private final Provider incomingRootResource; + + @Inject + public RepositoryBasedResourceProvider( + Provider tagRootResource, + Provider branchRootResource, + Provider changesetRootResource, + Provider sourceRootResource, + Provider contentResource, + Provider permissionRootResource, + Provider diffRootResource, + Provider modificationsRootResource, + Provider fileHistoryRootResource, + Provider incomingRootResource) { + this.tagRootResource = tagRootResource; + this.branchRootResource = branchRootResource; + this.changesetRootResource = changesetRootResource; + this.sourceRootResource = sourceRootResource; + this.contentResource = contentResource; + this.permissionRootResource = permissionRootResource; + this.diffRootResource = diffRootResource; + this.modificationsRootResource = modificationsRootResource; + this.fileHistoryRootResource = fileHistoryRootResource; + this.incomingRootResource = incomingRootResource; + } + + public TagRootResource getTagRootResource() { + return tagRootResource.get(); + } + + public BranchRootResource getBranchRootResource() { + return branchRootResource.get(); + } + + public ChangesetRootResource getChangesetRootResource() { + return changesetRootResource.get(); + } + + public SourceRootResource getSourceRootResource() { + return sourceRootResource.get(); + } + + public ContentResource getContentResource() { + return contentResource.get(); + } + + public RepositoryPermissionRootResource getPermissionRootResource() { + return permissionRootResource.get(); + } + + public DiffRootResource getDiffRootResource() { + return diffRootResource.get(); + } + + public ModificationsRootResource getModificationsRootResource() { + return modificationsRootResource.get(); + } + + public FileHistoryRootResource getFileHistoryRootResource() { + return fileHistoryRootResource.get(); + } + + public IncomingRootResource getIncomingRootResource() { + return incomingRootResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 9cd85b769c..711e1b16c9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -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 io.swagger.v3.oas.annotations.Operation; @@ -58,46 +58,19 @@ public class RepositoryResource { private final RepositoryManager manager; private final SingleResourceManagerAdapter adapter; - private final Provider tagRootResource; - private final Provider branchRootResource; - private final Provider changesetRootResource; - private final Provider sourceRootResource; - private final Provider contentResource; - private final Provider permissionRootResource; - private final Provider diffRootResource; - private final Provider modificationsRootResource; - private final Provider fileHistoryRootResource; - private final Provider incomingRootResource; + private final RepositoryBasedResourceProvider resourceProvider; @Inject public RepositoryResource( RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, RepositoryManager manager, - Provider tagRootResource, - Provider branchRootResource, - Provider changesetRootResource, - Provider sourceRootResource, Provider contentResource, - Provider permissionRootResource, - Provider diffRootResource, - Provider modificationsRootResource, - Provider fileHistoryRootResource, - Provider incomingRootResource + RepositoryBasedResourceProvider resourceProvider ) { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager, Repository.class); - this.tagRootResource = tagRootResource; - this.branchRootResource = branchRootResource; - this.changesetRootResource = changesetRootResource; - this.sourceRootResource = sourceRootResource; - this.contentResource = contentResource; - this.permissionRootResource = permissionRootResource; - this.diffRootResource = diffRootResource; - this.modificationsRootResource = modificationsRootResource; - this.fileHistoryRootResource = fileHistoryRootResource; - this.incomingRootResource = incomingRootResource; - + this.resourceProvider = resourceProvider; } /** @@ -211,52 +184,52 @@ public class RepositoryResource { @Path("tags/") public TagRootResource tags() { - return tagRootResource.get(); + return resourceProvider.getTagRootResource(); } @Path("diff/") public DiffRootResource diff() { - return diffRootResource.get(); + return resourceProvider.getDiffRootResource(); } @Path("branches/") public BranchRootResource branches() { - return branchRootResource.get(); + return resourceProvider.getBranchRootResource(); } @Path("changesets/") public ChangesetRootResource changesets() { - return changesetRootResource.get(); + return resourceProvider.getChangesetRootResource(); } @Path("history/") public FileHistoryRootResource history() { - return fileHistoryRootResource.get(); + return resourceProvider.getFileHistoryRootResource(); } @Path("sources/") public SourceRootResource sources() { - return sourceRootResource.get(); + return resourceProvider.getSourceRootResource(); } @Path("content/") public ContentResource content() { - return contentResource.get(); + return resourceProvider.getContentResource(); } @Path("permissions/") public RepositoryPermissionRootResource permissions() { - return permissionRootResource.get(); + return resourceProvider.getPermissionRootResource(); } @Path("modifications/") public ModificationsRootResource modifications() { - return modificationsRootResource.get(); + return resourceProvider.getModificationsRootResource(); } @Path("incoming/") public IncomingRootResource incoming() { - return incomingRootResource.get(); + return resourceProvider.getIncomingRootResource(); } private Supplier loadBy(String namespace, String name) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index 2be9c6cdef..4178b0664f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -103,8 +103,6 @@ public class BranchRootResourceTest extends RepositoryTestBase { private BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; - private BranchRootResource branchRootResource; - @Mock private BranchCollectionToDtoMapper branchCollectionToDtoMapper; @@ -126,7 +124,6 @@ public class BranchRootResourceTest extends RepositoryTestBase { changesetCollectionToDtoMapper = new BranchChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper, resourceLinks); - super.branchRootResource = Providers.of(branchRootResource); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java index 14b26396d4..319ca0944d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -91,8 +91,6 @@ public class ChangesetRootResourceTest extends RepositoryTestBase { @InjectMocks private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; - private ChangesetRootResource changesetRootResource; - private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -100,7 +98,6 @@ public class ChangesetRootResourceTest extends RepositoryTestBase { public void prepareEnvironment() { changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper); - super.changesetRootResource = Providers.of(changesetRootResource); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java index e5f3f97bb3..1e351f0510 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java @@ -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; @@ -90,9 +90,6 @@ public class DiffResourceTest extends RepositoryTestBase { @Mock private DiffResultToDiffResultDtoMapper diffResultToDiffResultDtoMapper; - private DiffRootResource diffRootResource; - - private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -100,7 +97,6 @@ public class DiffResourceTest extends RepositoryTestBase { @Before public void prepareEnvironment() { diffRootResource = new DiffRootResource(serviceFactory, diffResultToDiffResultDtoMapper); - super.diffRootResource = Providers.of(diffRootResource); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java index 77c25dd0a2..b24e4ec618 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java @@ -92,8 +92,6 @@ public class FileHistoryResourceTest extends RepositoryTestBase { @InjectMocks private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; - private FileHistoryRootResource fileHistoryRootResource; - private RestDispatcher dispatcher = new RestDispatcher(); private final Subject subject = mock(Subject.class); @@ -103,7 +101,6 @@ public class FileHistoryResourceTest extends RepositoryTestBase { public void prepareEnvironment() { fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper); - super.fileHistoryRootResource = Providers.of(fileHistoryRootResource); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java index b2ef5ad319..f2a5fe02b0 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java @@ -110,9 +110,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase { @InjectMocks private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; - private IncomingRootResource incomingRootResource; - - private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -121,7 +118,6 @@ public class IncomingRootResourceTest extends RepositoryTestBase { public void prepareEnvironment() { incomingChangesetCollectionToDtoMapper = new IncomingChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); incomingRootResource = new IncomingRootResource(serviceFactory, incomingChangesetCollectionToDtoMapper, diffResultToDiffResultDtoMapper); - super.incomingRootResource = Providers.of(incomingRootResource); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(REPOSITORY)).thenReturn(repositoryService); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java index 9c6a3b984c..d772fb0764 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java @@ -88,9 +88,6 @@ public class ModificationsResourceTest extends RepositoryTestBase { @InjectMocks private ModificationsToDtoMapperImpl modificationsToDtoMapper; - private ModificationsRootResource modificationsRootResource; - - private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -98,7 +95,6 @@ public class ModificationsResourceTest extends RepositoryTestBase { @Before public void prepareEnvironment() { modificationsRootResource = new ModificationsRootResource(serviceFactory, modificationsToDtoMapper); - super.modificationsRootResource = Providers.of(modificationsRootResource); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java index 86ffb719f0..d929bd4ebf 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java @@ -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.fasterxml.jackson.databind.JsonNode; @@ -144,8 +144,6 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper; - private RepositoryPermissionRootResource repositoryPermissionRootResource; - private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -154,8 +152,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { public void prepareEnvironment() { initMocks(this); repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); - repositoryPermissionRootResource = new RepositoryPermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager); - super.permissionRootResource = Providers.of(repositoryPermissionRootResource); + permissionRootResource = new RepositoryPermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager); dispatcher.addSingletonResource(getRepositoryRootResource()); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index faa0455f07..bcc1d2bc3f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -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.github.sdorra.shiro.ShiroRule; @@ -122,7 +122,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { super.dtoToRepositoryMapper = dtoToRepositoryMapper; super.manager = repositoryManager; RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); - super.repositoryCollectionResource = Providers.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer)); + super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(scmPathInfoStore.get()).thenReturn(uriInfo); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java index f7a9547c8c..0d4dc09e10 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java @@ -21,49 +21,52 @@ * 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; import sonia.scm.repository.RepositoryManager; -import javax.inject.Provider; +import static com.google.inject.util.Providers.of; +import static org.mockito.Mockito.mock; -public abstract class RepositoryTestBase { +abstract class RepositoryTestBase { - - protected RepositoryToRepositoryDtoMapper repositoryToDtoMapper; - protected RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; - protected RepositoryManager manager; - protected Provider tagRootResource; - protected Provider branchRootResource; - protected Provider changesetRootResource; - protected Provider sourceRootResource; - protected Provider contentResource; - protected Provider permissionRootResource; - protected Provider diffRootResource; - protected Provider modificationsRootResource; - protected Provider fileHistoryRootResource; - protected Provider repositoryCollectionResource; - protected Provider incomingRootResource; + RepositoryToRepositoryDtoMapper repositoryToDtoMapper; + RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; + RepositoryManager manager; + TagRootResource tagRootResource; + BranchRootResource branchRootResource; + ChangesetRootResource changesetRootResource; + SourceRootResource sourceRootResource; + ContentResource contentResource; + RepositoryPermissionRootResource permissionRootResource; + DiffRootResource diffRootResource; + ModificationsRootResource modificationsRootResource; + FileHistoryRootResource fileHistoryRootResource; + IncomingRootResource incomingRootResource; + RepositoryCollectionResource repositoryCollectionResource; RepositoryRootResource getRepositoryRootResource() { - return new RepositoryRootResource(Providers.of(new RepositoryResource( - repositoryToDtoMapper, - dtoToRepositoryMapper, - manager, - tagRootResource, - branchRootResource, - changesetRootResource, - sourceRootResource, - contentResource, - permissionRootResource, - diffRootResource, - modificationsRootResource, - fileHistoryRootResource, - incomingRootResource)), repositoryCollectionResource); + RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider( + of(tagRootResource), + of(branchRootResource), + of(changesetRootResource), + of(sourceRootResource), + of(contentResource), + of(permissionRootResource), + of(diffRootResource), + of(modificationsRootResource), + of(fileHistoryRootResource), + of(incomingRootResource) + ); + return new RepositoryRootResource( + of(new RepositoryResource( + repositoryToDtoMapper, + dtoToRepositoryMapper, + manager, + repositoryBasedResourceProvider + )), + of(repositoryCollectionResource)); } - - } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 311a54b4f0..c3f6d5b185 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -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; @@ -74,8 +74,7 @@ public class SourceRootResourceTest extends RepositoryTestBase { when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(service.getBrowseCommand()).thenReturn(browseCommandBuilder); - SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToFileObjectDtoMapper); - super.sourceRootResource = Providers.of(sourceRootResource); + sourceRootResource = new SourceRootResource(serviceFactory, browserResultToFileObjectDtoMapper); dispatcher.addSingletonResource(getRepositoryRootResource()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java index 11088c8017..726cbc89e4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java @@ -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; @@ -84,9 +84,6 @@ public class TagRootResourceTest extends RepositoryTestBase { @InjectMocks private TagToTagDtoMapperImpl tagToTagDtoMapper; - private TagRootResource tagRootResource; - - private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -95,7 +92,6 @@ public class TagRootResourceTest extends RepositoryTestBase { public void prepareEnvironment() throws Exception { tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper); tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper); - super.tagRootResource = Providers.of(tagRootResource); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); From c16c4893aeadb14cc438ca3319e77e6a09c5d8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 11 Jun 2020 15:43:25 +0200 Subject: [PATCH 02/26] Use new fct. getMapperClass of mapstruct --- .../scm/api/v2/resources/MapperModule.java | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 83be1f3636..d77533e01d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -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.AbstractModule; @@ -32,46 +32,46 @@ import sonia.scm.web.api.RepositoryToHalMapper; public class MapperModule extends AbstractModule { @Override protected void configure() { - bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass()); - bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass()); + bind(UserDtoToUserMapper.class).to(Mappers.getMapperClass(UserDtoToUserMapper.class)); + bind(UserToUserDtoMapper.class).to(Mappers.getMapperClass(UserToUserDtoMapper.class)); bind(UserCollectionToDtoMapper.class); - bind(GroupDtoToGroupMapper.class).to(Mappers.getMapper(GroupDtoToGroupMapper.class).getClass()); - bind(GroupToGroupDtoMapper.class).to(Mappers.getMapper(GroupToGroupDtoMapper.class).getClass()); + bind(GroupDtoToGroupMapper.class).to(Mappers.getMapperClass(GroupDtoToGroupMapper.class)); + bind(GroupToGroupDtoMapper.class).to(Mappers.getMapperClass(GroupToGroupDtoMapper.class)); bind(GroupCollectionToDtoMapper.class); - bind(ScmConfigurationToConfigDtoMapper.class).to(Mappers.getMapper(ScmConfigurationToConfigDtoMapper.class).getClass()); - bind(ConfigDtoToScmConfigurationMapper.class).to(Mappers.getMapper(ConfigDtoToScmConfigurationMapper.class).getClass()); + bind(ScmConfigurationToConfigDtoMapper.class).to(Mappers.getMapperClass(ScmConfigurationToConfigDtoMapper.class)); + bind(ConfigDtoToScmConfigurationMapper.class).to(Mappers.getMapperClass(ConfigDtoToScmConfigurationMapper.class)); - bind(RepositoryToRepositoryDtoMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass()); - bind(RepositoryDtoToRepositoryMapper.class).to(Mappers.getMapper(RepositoryDtoToRepositoryMapper.class).getClass()); + bind(RepositoryToRepositoryDtoMapper.class).to(Mappers.getMapperClass(RepositoryToRepositoryDtoMapper.class)); + bind(RepositoryDtoToRepositoryMapper.class).to(Mappers.getMapperClass(RepositoryDtoToRepositoryMapper.class)); - bind(RepositoryTypeToRepositoryTypeDtoMapper.class).to(Mappers.getMapper(RepositoryTypeToRepositoryTypeDtoMapper.class).getClass()); + bind(RepositoryTypeToRepositoryTypeDtoMapper.class).to(Mappers.getMapperClass(RepositoryTypeToRepositoryTypeDtoMapper.class)); bind(RepositoryTypeCollectionToDtoMapper.class); - bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); - bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass()); - bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass()); + bind(BranchToBranchDtoMapper.class).to(Mappers.getMapperClass(BranchToBranchDtoMapper.class)); + bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapperClass(RepositoryPermissionDtoToRepositoryPermissionMapper.class)); + bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapperClass(RepositoryPermissionToRepositoryPermissionDtoMapper.class)); - bind(RepositoryRoleToRepositoryRoleDtoMapper.class).to(Mappers.getMapper(RepositoryRoleToRepositoryRoleDtoMapper.class).getClass()); - bind(RepositoryRoleDtoToRepositoryRoleMapper.class).to(Mappers.getMapper(RepositoryRoleDtoToRepositoryRoleMapper.class).getClass()); + bind(RepositoryRoleToRepositoryRoleDtoMapper.class).to(Mappers.getMapperClass(RepositoryRoleToRepositoryRoleDtoMapper.class)); + bind(RepositoryRoleDtoToRepositoryRoleMapper.class).to(Mappers.getMapperClass(RepositoryRoleDtoToRepositoryRoleMapper.class)); bind(RepositoryRoleCollectionToDtoMapper.class); - bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(DefaultChangesetToChangesetDtoMapper.class).getClass()); - bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass()); + bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapperClass(DefaultChangesetToChangesetDtoMapper.class)); + bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapperClass(ChangesetToParentDtoMapper.class)); - bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass()); + bind(TagToTagDtoMapper.class).to(Mappers.getMapperClass(TagToTagDtoMapper.class)); - bind(BrowserResultToFileObjectDtoMapper.class).to(Mappers.getMapper(BrowserResultToFileObjectDtoMapper.class).getClass()); - bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass()); + bind(BrowserResultToFileObjectDtoMapper.class).to(Mappers.getMapperClass(BrowserResultToFileObjectDtoMapper.class)); + bind(ModificationsToDtoMapper.class).to(Mappers.getMapperClass(ModificationsToDtoMapper.class)); - bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass()); + bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapperClass(ReducedObjectModelToDtoMapper.class)); - bind(ResteasyViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ResteasyViolationExceptionToErrorDtoMapper.class).getClass()); - bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass()); - bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass()); + bind(ResteasyViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapperClass(ResteasyViolationExceptionToErrorDtoMapper.class)); + bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapperClass(ScmViolationExceptionToErrorDtoMapper.class)); + bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapperClass(ExceptionWithContextToErrorDtoMapper.class)); - bind(RepositoryToHalMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass()); + bind(RepositoryToHalMapper.class).to(Mappers.getMapperClass(RepositoryToRepositoryDtoMapper.class)); // no mapstruct required bind(MeDtoFactory.class); @@ -80,6 +80,6 @@ public class MapperModule extends AbstractModule { bind(ScmPathInfoStore.class).in(ServletScopes.REQUEST); - bind(PluginDtoMapper.class).to(Mappers.getMapper(PluginDtoMapper.class).getClass()); + bind(PluginDtoMapper.class).to(Mappers.getMapperClass(PluginDtoMapper.class)); } } From 4cb898edbb2813f7950e7b2a4c7182929440094a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 11 Jun 2020 17:21:25 +0200 Subject: [PATCH 03/26] Add annotate rest resource --- .../main/java/sonia/scm/web/VndMediaType.java | 5 +- .../api/v2/resources/AnnotateResource.java | 95 +++++++++++ .../v2/resources/BaseFileObjectDtoMapper.java | 3 +- .../sonia/scm/api/v2/resources/BlameDto.java | 42 +++++ .../scm/api/v2/resources/BlameLineDto.java | 41 +++++ .../BlameResultToBlameDtoMapper.java | 72 +++++++++ .../scm/api/v2/resources/MapperModule.java | 2 + .../RepositoryBasedResourceProvider.java | 8 +- .../api/v2/resources/RepositoryResource.java | 5 + .../scm/api/v2/resources/ResourceLinks.java | 18 ++- .../v2/resources/AnnotateResourceTest.java | 147 ++++++++++++++++++ .../api/v2/resources/RepositoryTestBase.java | 5 +- .../api/v2/resources/ResourceLinksMock.java | 84 +++++----- 13 files changed, 478 insertions(+), 49 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AnnotateResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameLineDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameResultToBlameDtoMapper.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/AnnotateResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index af4e65caf0..14fd4b4532 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; import javax.ws.rs.core.MediaType; @@ -71,13 +71,12 @@ public class VndMediaType { @SuppressWarnings("squid:S2068") public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX; public static final String PERMISSION_COLLECTION = PREFIX + "permissionCollection" + SUFFIX; - public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX; - public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX; public static final String NAMESPACE_STRATEGIES = PREFIX + "namespaceStrategies" + SUFFIX; public static final String ME = PREFIX + "me" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX; + public static final String ANNOTATE = PREFIX + "annotate" + SUFFIX; public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX; public static final String REPOSITORY_ROLE = PREFIX + "repositoryRole" + SUFFIX; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AnnotateResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AnnotateResource.java new file mode 100644 index 0000000000..43f8d87130 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AnnotateResource.java @@ -0,0 +1,95 @@ +/* + * 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 io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import java.io.IOException; + +public class AnnotateResource { + + private final RepositoryServiceFactory serviceFactory; + private final BlameResultToBlameDtoMapper mapper; + + @Inject + public AnnotateResource(RepositoryServiceFactory serviceFactory, BlameResultToBlameDtoMapper mapper) { + this.serviceFactory = serviceFactory; + this.mapper = mapper; + } + + /** + * Returns the content of a file with additional information for each line regarding the last commit that + * changed this line: The revision, the author, the date, and the description of the commit. + * + * @param namespace the namespace of the repository + * @param name the name of the repository + * @param revision the revision + * @param path The path of the file + */ + @GET + @Path("{revision}/{path: .*}") + @Produces(VndMediaType.ANNOTATE) + @Operation(summary = "File content by revision", description = "Returns the annotated file for the given revision in the repository.", tags = "Repository") + @ApiResponse(responseCode = "200", description = "success") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the repository") + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified name available in the namespace", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + public BlameDto annotate( + @PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("revision") String revision, + @PathParam("path") String path + ) throws IOException { + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); + try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { + return mapper.map(repositoryService.getBlameCommand().setRevision(revision).getBlameResult(path), namespaceAndName, revision, path); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java index ab065762e2..9e6f82a549 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java @@ -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.common.annotations.VisibleForTesting; @@ -63,6 +63,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst } else { links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)); links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); + links.single(link("annotate", resourceLinks.annotate().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); } if (fileObject.isTruncated()) { links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?offset=" + (offset + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameDto.java new file mode 100644 index 0000000000..8aec147a85 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameDto.java @@ -0,0 +1,42 @@ +/* + * 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 de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Data; + +import java.util.List; + +@Data +public class BlameDto extends HalRepresentation { + + private List blameLines; + + public BlameDto(Links links) { + super(links); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameLineDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameLineDto.java new file mode 100644 index 0000000000..29e3d785b6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameLineDto.java @@ -0,0 +1,41 @@ +/* + * 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.Data; + +import java.time.Instant; + +@Data +public class BlameLineDto { + + private PersonDto author; + private String code; + private String description; + private int lineNumber; + private String revision; + private Instant when; + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameResultToBlameDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameResultToBlameDtoMapper.java new file mode 100644 index 0000000000..4758d4ba97 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameResultToBlameDtoMapper.java @@ -0,0 +1,72 @@ +/* + * 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.annotations.VisibleForTesting; +import de.otto.edison.hal.Links; +import org.mapstruct.Mapper; +import org.mapstruct.ObjectFactory; +import sonia.scm.repository.BlameLine; +import sonia.scm.repository.BlameResult; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; + +import javax.inject.Inject; +import java.util.stream.Collectors; + +@Mapper +public abstract class BlameResultToBlameDtoMapper implements InstantAttributeMapper { + + @Inject + private ResourceLinks resourceLinks; + + BlameDto map(BlameResult result, NamespaceAndName namespaceAndName, String revision, String path) { + BlameDto dto = createDto(namespaceAndName, revision, path); + dto.setBlameLines(result.getBlameLines().stream().map(this::map).collect(Collectors.toList())); + return dto; + } + + abstract BlameLineDto map(BlameLine line); + + abstract PersonDto map(Person person); + + @ObjectFactory + BlameDto createDto(NamespaceAndName namespaceAndName, String revision, String path) { + return new BlameDto(Links.linkingTo() + .self( + resourceLinks.annotate() + .self( + namespaceAndName.getNamespace(), + namespaceAndName.getName(), + revision, + path)) + .build()); + } + + @VisibleForTesting + void setResourceLinks(ResourceLinks resourceLinks) { + this.resourceLinks = resourceLinks; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index d77533e01d..a45b52ddbe 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -73,6 +73,8 @@ public class MapperModule extends AbstractModule { bind(RepositoryToHalMapper.class).to(Mappers.getMapperClass(RepositoryToRepositoryDtoMapper.class)); + bind(BlameResultToBlameDtoMapper.class).to(Mappers.getMapperClass(BlameResultToBlameDtoMapper.class)); + // no mapstruct required bind(MeDtoFactory.class); bind(UIPluginDtoMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java index 96580a717c..9b779ebaa6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java @@ -38,6 +38,7 @@ public class RepositoryBasedResourceProvider { private final Provider modificationsRootResource; private final Provider fileHistoryRootResource; private final Provider incomingRootResource; + private final Provider annotateResource; @Inject public RepositoryBasedResourceProvider( @@ -50,7 +51,7 @@ public class RepositoryBasedResourceProvider { Provider diffRootResource, Provider modificationsRootResource, Provider fileHistoryRootResource, - Provider incomingRootResource) { + Provider incomingRootResource, Provider annotateResource) { this.tagRootResource = tagRootResource; this.branchRootResource = branchRootResource; this.changesetRootResource = changesetRootResource; @@ -61,6 +62,7 @@ public class RepositoryBasedResourceProvider { this.modificationsRootResource = modificationsRootResource; this.fileHistoryRootResource = fileHistoryRootResource; this.incomingRootResource = incomingRootResource; + this.annotateResource = annotateResource; } public TagRootResource getTagRootResource() { @@ -102,4 +104,8 @@ public class RepositoryBasedResourceProvider { public IncomingRootResource getIncomingRootResource() { return incomingRootResource.get(); } + + public AnnotateResource getAnnotateResource() { + return annotateResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 711e1b16c9..2ac4f0674f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -232,6 +232,11 @@ public class RepositoryResource { return resourceProvider.getIncomingRootResource(); } + @Path("annotate/") + public AnnotateResource annotate() { + return resourceProvider.getAnnotateResource(); + } + private Supplier loadBy(String namespace, String name) { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 1b667602f0..48842edb92 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -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 sonia.scm.repository.NamespaceAndName; @@ -566,6 +566,22 @@ class ResourceLinks { } } + public AnnotateLinks annotate() { + return new AnnotateLinks(scmPathInfoStore.get()); + } + + static class AnnotateLinks { + private final LinkBuilder annotateLinkBuilder; + + AnnotateLinks(ScmPathInfo pathInfo) { + this.annotateLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, AnnotateResource.class); + } + + String self(String namespace, String name, String revision, String path) { + return annotateLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("annotate").parameters().method("annotate").parameters(revision, path).href(); + } + } + RepositoryVerbLinks repositoryVerbs() { return new RepositoryVerbLinks(scmPathInfoStore.get()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AnnotateResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AnnotateResourceTest.java new file mode 100644 index 0000000000..af64d4dad3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AnnotateResourceTest.java @@ -0,0 +1,147 @@ +/* + * 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.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.BlameLine; +import sonia.scm.repository.BlameResult; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; +import sonia.scm.repository.api.BlameCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.RestDispatcher; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AnnotateResourceTest extends RepositoryTestBase { + + public static final NamespaceAndName NAMESPACE_AND_NAME = new NamespaceAndName("space", "X"); + public static final String REVISION = "123"; + public static final String PATH = "some/file"; + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService service; + @Mock + private BlameCommandBuilder blameCommandBuilder; + + private final RestDispatcher dispatcher = new RestDispatcher(); + private final MockHttpResponse response = new MockHttpResponse(); + + @BeforeEach + void initResource() { + BlameResultToBlameDtoMapperImpl mapper = new BlameResultToBlameDtoMapperImpl(); + mapper.setResourceLinks(ResourceLinksMock.createMock(URI.create("/"))); + annotateResource = new AnnotateResource(serviceFactory, mapper); + dispatcher.addSingletonResource(getRepositoryRootResource()); + } + + @BeforeEach + void initRepositoryService() { + when(serviceFactory.create(NAMESPACE_AND_NAME)).thenReturn(service); + when(service.getBlameCommand()).thenReturn(blameCommandBuilder); + } + + @BeforeEach + void initBlameCommand() throws IOException { + BlameLine line1 = new BlameLine( + 0, + "100", + System.currentTimeMillis(), + new Person("Arthur Dent", "arthur@hitchhiker.com"), + "first try", + "jump" + ); + BlameLine line2 = new BlameLine( + 1, + "42", + System.currentTimeMillis(), + new Person("Zaphod Beeblebrox", "zaphod@hitchhiker.com"), + "got it", + "heart of gold" + ); + BlameResult result = new BlameResult(asList(line1, line2)); + when(blameCommandBuilder.setRevision(REVISION)).thenReturn(blameCommandBuilder); + when(blameCommandBuilder.getBlameResult(PATH)).thenReturn(result); + } + + @Test + void test() throws URISyntaxException, UnsupportedEncodingException, JsonProcessingException { + MockHttpRequest request = MockHttpRequest + .get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + NAMESPACE_AND_NAME + "/annotate/" + REVISION + "/" + PATH); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(200); + + String content = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(content); + JsonNode blameLines = jsonNode.get("blameLines"); + assertThat(blameLines.isArray()).isTrue(); + assertThat(jsonNode.get("_links").get("self").get("href").asText()) + .isEqualTo("/v2/repositories/space/X/annotate/123/some%2Ffile"); + + Iterator lineIterator = blameLines.iterator(); + JsonNode line1 = lineIterator.next(); + assertThat(line1.get("author").get("mail").asText()).isEqualTo("arthur@hitchhiker.com"); + assertThat(line1.get("author").get("name").asText()).isEqualTo("Arthur Dent"); + assertThat(line1.get("code").asText()).isEqualTo("jump"); + assertThat(line1.get("description").asText()).isEqualTo("first try"); + assertThat(line1.get("lineNumber").asInt()).isEqualTo(0); + assertThat(line1.get("revision").asText()).isEqualTo("100"); + + JsonNode line2 = lineIterator.next(); + assertThat(line2.get("author").get("mail").asText()).isEqualTo("zaphod@hitchhiker.com"); + assertThat(line2.get("author").get("name").asText()).isEqualTo("Zaphod Beeblebrox"); + assertThat(line2.get("code").asText()).isEqualTo("heart of gold"); + assertThat(line2.get("description").asText()).isEqualTo("got it"); + assertThat(line2.get("lineNumber").asInt()).isEqualTo(1); + assertThat(line2.get("revision").asText()).isEqualTo("42"); + + assertThat(lineIterator.hasNext()).isFalse(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java index 0d4dc09e10..bafb92fd62 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java @@ -45,6 +45,7 @@ abstract class RepositoryTestBase { FileHistoryRootResource fileHistoryRootResource; IncomingRootResource incomingRootResource; RepositoryCollectionResource repositoryCollectionResource; + AnnotateResource annotateResource; RepositoryRootResource getRepositoryRootResource() { @@ -58,8 +59,8 @@ abstract class RepositoryTestBase { of(diffRootResource), of(modificationsRootResource), of(fileHistoryRootResource), - of(incomingRootResource) - ); + of(incomingRootResource), + of(annotateResource)); return new RepositoryRootResource( of(new RepositoryResource( repositoryToDtoMapper, diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 39a9d2e30c..96cf705625 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -21,11 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import java.net.URI; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,47 +34,48 @@ public class ResourceLinksMock { public static ResourceLinks createMock(URI baseUri) { ResourceLinks resourceLinks = mock(ResourceLinks.class); - ScmPathInfo uriInfo = mock(ScmPathInfo.class); - when(uriInfo.getApiRestUri()).thenReturn(baseUri); + ScmPathInfo pathInfo = mock(ScmPathInfo.class); + when(pathInfo.getApiRestUri()).thenReturn(baseUri); - ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(uriInfo); - when(resourceLinks.user()).thenReturn(userLinks); - when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks)); - when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo)); - when(resourceLinks.userPermissions()).thenReturn(new ResourceLinks.UserPermissionLinks(uriInfo)); - when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(uriInfo)); - when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); - when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); - when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(uriInfo)); - when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); - when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(uriInfo)); - when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo)); - when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); - when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); - when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); - when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(uriInfo)); - when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); - when(resourceLinks.repositoryPermission()).thenReturn(new ResourceLinks.RepositoryPermissionLinks(uriInfo)); - when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); - when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); - when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(uriInfo)); - when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(uriInfo)); - when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); - when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); - when(resourceLinks.installedPluginCollection()).thenReturn(new ResourceLinks.InstalledPluginCollectionLinks(uriInfo)); - when(resourceLinks.availablePluginCollection()).thenReturn(new ResourceLinks.AvailablePluginCollectionLinks(uriInfo)); - when(resourceLinks.pendingPluginCollection()).thenReturn(new ResourceLinks.PendingPluginCollectionLinks(uriInfo)); - when(resourceLinks.installedPlugin()).thenReturn(new ResourceLinks.InstalledPluginLinks(uriInfo)); - when(resourceLinks.availablePlugin()).thenReturn(new ResourceLinks.AvailablePluginLinks(uriInfo)); - when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo)); - when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo)); - when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo)); - when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo)); - when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); - when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(uriInfo)); - when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo)); - when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo)); - when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); + ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(pathInfo); + lenient().when(resourceLinks.user()).thenReturn(userLinks); + lenient().when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(pathInfo,userLinks)); + lenient().when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(pathInfo)); + lenient().when(resourceLinks.userPermissions()).thenReturn(new ResourceLinks.UserPermissionLinks(pathInfo)); + lenient().when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(pathInfo)); + lenient().when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(pathInfo)); + lenient().when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(pathInfo)); + lenient().when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(pathInfo)); + lenient().when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(pathInfo)); + lenient().when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(pathInfo)); + lenient().when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(pathInfo)); + lenient().when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(pathInfo)); + lenient().when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(pathInfo)); + lenient().when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(pathInfo)); + lenient().when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(pathInfo)); + lenient().when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(pathInfo)); + lenient().when(resourceLinks.repositoryPermission()).thenReturn(new ResourceLinks.RepositoryPermissionLinks(pathInfo)); + lenient().when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(pathInfo)); + lenient().when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(pathInfo)); + lenient().when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(pathInfo)); + lenient().when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(pathInfo)); + lenient().when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(pathInfo)); + lenient().when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(pathInfo)); + lenient().when(resourceLinks.installedPluginCollection()).thenReturn(new ResourceLinks.InstalledPluginCollectionLinks(pathInfo)); + lenient().when(resourceLinks.availablePluginCollection()).thenReturn(new ResourceLinks.AvailablePluginCollectionLinks(pathInfo)); + lenient().when(resourceLinks.pendingPluginCollection()).thenReturn(new ResourceLinks.PendingPluginCollectionLinks(pathInfo)); + lenient().when(resourceLinks.installedPlugin()).thenReturn(new ResourceLinks.InstalledPluginLinks(pathInfo)); + lenient().when(resourceLinks.availablePlugin()).thenReturn(new ResourceLinks.AvailablePluginLinks(pathInfo)); + lenient().when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(pathInfo)); + lenient().when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(pathInfo)); + lenient().when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(pathInfo)); + lenient().when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(pathInfo)); + lenient().when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(pathInfo)); + lenient().when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(pathInfo)); + lenient().when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(pathInfo)); + lenient().when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(pathInfo)); + lenient().when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(pathInfo)); + lenient().when(resourceLinks.annotate()).thenReturn(new ResourceLinks.AnnotateLinks(pathInfo)); return resourceLinks; } From 9a66efc6934aa23b39cd64f597de91cee685635f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 12 Jun 2020 08:19:33 +0200 Subject: [PATCH 04/26] added first version of annotate ui component --- scm-ui/ui-components/package.json | 2 +- scm-ui/ui-components/src/Annotate.stories.tsx | 112 +++++++++++++++ scm-ui/ui-components/src/Annotate.tsx | 135 ++++++++++++++++++ scm-ui/ui-components/src/DateFromNow.tsx | 24 +--- scm-ui/ui-components/src/DateShort.tsx | 45 ++++++ scm-ui/ui-components/src/dates.ts | 19 +++ scm-ui/ui-components/src/index.ts | 2 + yarn.lock | 14 +- 8 files changed, 331 insertions(+), 22 deletions(-) create mode 100644 scm-ui/ui-components/src/Annotate.stories.tsx create mode 100644 scm-ui/ui-components/src/Annotate.tsx create mode 100644 scm-ui/ui-components/src/DateShort.tsx create mode 100644 scm-ui/ui-components/src/dates.ts diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 7390623378..23ed2b76ba 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -60,7 +60,7 @@ "react-markdown": "^4.0.6", "react-router-dom": "^5.1.2", "react-select": "^2.1.2", - "react-syntax-highlighter": "^11.0.2" + "react-syntax-highlighter": "https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7" }, "babel": { "presets": [ diff --git a/scm-ui/ui-components/src/Annotate.stories.tsx b/scm-ui/ui-components/src/Annotate.stories.tsx new file mode 100644 index 0000000000..582c7545d0 --- /dev/null +++ b/scm-ui/ui-components/src/Annotate.stories.tsx @@ -0,0 +1,112 @@ +/* + * 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 Annotate, { AnnotatedSource } from "./Annotate"; + +const Wrapper = styled.div` + margin: 2rem; +`; + +const commitCreateNewApp = { + revision: "0d8c1d328f4599b363755671afe667c7ace52bae", + author: { + name: "Arthur Dent", + mail: "arthur.dent@hitchhiker.com" + }, + description: "create new app", + when: new Date() +}; + +const commitFixedMissingImport = { + revision: "fab38559ce3ab8c388e067712b4bd7ab94b9fa9b", + author: { + name: "Tricia Marie McMillan", + mail: "trillian@hitchhiker.com" + }, + description: "fixed missing import", + when: new Date() +}; + +const commitImplementMain = { + revision: "5203292ab2bc0c020dd22adc4d3897da4930e43f", + author: { + name: "Ford Prefect", + mail: "ford.prefect@hitchhiker.com" + }, + description: "implemented main function", + when: new Date() +}; + +const source: AnnotatedSource = { + language: "go", + lines: [ + { + lineNumber: 1, + code: "package main", + ...commitCreateNewApp + }, + { + lineNumber: 2, + code: "", + ...commitCreateNewApp + }, + { + lineNumber: 3, + code: 'import "fmt"', + ...commitFixedMissingImport + }, + { + lineNumber: 4, + code: "", + ...commitFixedMissingImport + }, + { + lineNumber: 5, + code: "func main() {", + ...commitCreateNewApp + }, + { + lineNumber: 6, + code: ' fmt.Println("Hello World")', + ...commitImplementMain + }, + { + lineNumber: 7, + code: "}", + ...commitCreateNewApp + }, + { + lineNumber: 8, + code: "", + ...commitCreateNewApp + } + ] +}; + +storiesOf("Annotate", module) + .addDecorator(storyFn => {storyFn()}) + .add("Default", () => ); diff --git a/scm-ui/ui-components/src/Annotate.tsx b/scm-ui/ui-components/src/Annotate.tsx new file mode 100644 index 0000000000..b71d78023b --- /dev/null +++ b/scm-ui/ui-components/src/Annotate.tsx @@ -0,0 +1,135 @@ +/* + * 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 { Person } from "@scm-manager/ui-types"; + +// @ts-ignore +import { LightAsync as ReactSyntaxHighlighter, createElement } from "react-syntax-highlighter"; + +// @ts-ignore +import { arduinoLight } from "react-syntax-highlighter/dist/cjs/styles/hljs"; +import styled from "styled-components"; +import DateShort from "./DateShort"; + +// TODO move types to ui-types + +export type AnnotatedSource = { + lines: AnnotatedLine[]; + language: string; +}; + +export type AnnotatedLine = { + author: Person; + code: string; + description: string; + lineNumber: number; + revision: string; + when: Date; +}; + +type Props = { + source: AnnotatedSource; +}; + +const Author = styled.a` + width: 8em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + float: left; +`; + +const LineNumber = styled.span` + width: 3em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + border-left: 1px solid lightgrey; + border-right: 1px solid lightgrey; + + text-align: right; + float: left; + + padding: 0 0.5em; +`; + +const When = styled.span` + width: 90px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + float: left; + + margin: 0 0.5em; +`; + +const Annotate: FC = ({ source }) => { + // @ts-ignore + const defaultRenderer = ({ rows, stylesheet, useInlineStyles }) => { + // @ts-ignore + return rows.map((node, i) => { + const line = createElement({ + node, + stylesheet, + useInlineStyles, + key: `code-segement${i}` + }); + + if (i + 1 < rows.length) { + const annotation = source.lines[i]; + return ( + + {annotation.author.name}{" "} + + + {" "} + {i + 1} {line} + + ); + } + + return line; + }); + }; + + const code = source.lines.reduce((content, line) => { + content += line.code + "\n"; + return content; + }, ""); + + return ( + + {code} + + ); +}; + +export default Annotate; diff --git a/scm-ui/ui-components/src/DateFromNow.tsx b/scm-ui/ui-components/src/DateFromNow.tsx index 6b68bd8bd2..eda6efdcc7 100644 --- a/scm-ui/ui-components/src/DateFromNow.tsx +++ b/scm-ui/ui-components/src/DateFromNow.tsx @@ -23,16 +23,14 @@ */ import React from "react"; import { withTranslation, WithTranslation } from "react-i18next"; -import { formatDistance, format, parseISO, Locale } from "date-fns"; +import { formatDistance, format, Locale } from "date-fns"; import { enUS, de, es } from "date-fns/locale"; -import styled from "styled-components"; +import { DateInput, DateElement, FullDateFormat, toDate } from "./dates"; type LocaleMap = { [key: string]: Locale; }; -type DateInput = Date | string; - export const supportedLocales: LocaleMap = { enUS, en: enUS, @@ -59,11 +57,6 @@ type Options = { timeZone?: string; }; -const DateElement = styled.time` - border-bottom: 1px dotted rgba(219, 219, 219); - cursor: help; -`; - export const chooseLocale = (language: string, languages?: string[]) => { for (const lng of languages || []) { const locale = supportedLocales[lng]; @@ -98,17 +91,10 @@ class DateFromNow extends React.Component { return options; }; - toDate = (value: DateInput): Date => { - if (value instanceof Date) { - return value; - } - return parseISO(value); - }; - getBaseDate = () => { const { baseDate } = this.props; if (baseDate) { - return this.toDate(baseDate); + return toDate(baseDate); } return new Date(); }; @@ -116,10 +102,10 @@ class DateFromNow extends React.Component { render() { const { date } = this.props; if (date) { - const isoDate = this.toDate(date); + const isoDate = toDate(date); const options = this.createOptions(); const distance = formatDistance(isoDate, this.getBaseDate(), options); - const formatted = format(isoDate, "yyyy-MM-dd HH:mm:ss", options); + const formatted = format(isoDate, FullDateFormat, options); return {distance}; } return null; diff --git a/scm-ui/ui-components/src/DateShort.tsx b/scm-ui/ui-components/src/DateShort.tsx new file mode 100644 index 0000000000..86510177ad --- /dev/null +++ b/scm-ui/ui-components/src/DateShort.tsx @@ -0,0 +1,45 @@ +/* + * 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 { format } from "date-fns"; +import { toDate, ShortDateFormat, FullDateFormat, DateElement } from "./dates"; + +type Props = { + value?: Date | string; + className?: string; +}; + +const DateShort: FC = ({ value, className }) => { + if (!value) { + return null; + } + const date = toDate(value); + return ( + + {format(date, ShortDateFormat)} + + ); +}; + +export default DateShort; diff --git a/scm-ui/ui-components/src/dates.ts b/scm-ui/ui-components/src/dates.ts new file mode 100644 index 0000000000..c335750ad2 --- /dev/null +++ b/scm-ui/ui-components/src/dates.ts @@ -0,0 +1,19 @@ +import styled from "styled-components"; +import { parseISO } from "date-fns"; + +export type DateInput = Date | string; + +export const ShortDateFormat = "yyyy-MM-dd"; +export const FullDateFormat = "yyyy-MM-dd HH:mm:ss"; + +export const DateElement = styled.time` + border-bottom: 1px dotted rgba(219, 219, 219); + cursor: help; +`; + +export const toDate = (value: DateInput): Date => { + if (value instanceof Date) { + return value; + } + return parseISO(value); +}; diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index 68ec1ec2ef..07f4bdf5f3 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -43,7 +43,9 @@ import { export { validation, urls, repositories }; +export { default as Annotate } from "./Annotate"; export { default as DateFromNow } from "./DateFromNow"; +export { default as DateShort } from "./DateShort"; export { default as ErrorNotification } from "./ErrorNotification"; export { default as ErrorPage } from "./ErrorPage"; export { default as Icon } from "./Icon"; diff --git a/yarn.lock b/yarn.lock index cb8829e58a..8651f444e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11809,7 +11809,7 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -prismjs@^1.8.4: +prismjs@^1.16.0, prismjs@^1.8.4: version "1.20.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03" integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ== @@ -12424,6 +12424,16 @@ react-syntax-highlighter@^11.0.2: prismjs "^1.8.4" refractor "^2.4.1" +"react-syntax-highlighter@https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7": + version "12.0.2" + resolved "https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7" + dependencies: + "@babel/runtime" "^7.3.1" + highlight.js "~9.13.0" + lowlight "~1.11.0" + prismjs "^1.16.0" + refractor "^2.10.1" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.10.2: version "16.13.1" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1" @@ -12736,7 +12746,7 @@ reflect.ownkeys@^0.2.0: resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= -refractor@^2.4.1: +refractor@^2.10.1, refractor@^2.4.1: version "2.10.1" resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e" integrity sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw== From e5153b4fa9c7332f74f8adf9dd6a0fa5b210fbf2 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 12 Jun 2020 09:08:35 +0200 Subject: [PATCH 05/26] display only new annotations --- scm-ui/ui-components/src/Annotate.tsx | 36 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/scm-ui/ui-components/src/Annotate.tsx b/scm-ui/ui-components/src/Annotate.tsx index b71d78023b..e0e1768611 100644 --- a/scm-ui/ui-components/src/Annotate.tsx +++ b/scm-ui/ui-components/src/Annotate.tsx @@ -53,12 +53,29 @@ type Props = { source: AnnotatedSource; }; -const Author = styled.a` +type LineElementProps = { + newAnnotation: boolean; +}; + +const Author = styled.a` width: 8em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; float: left; + + visibility: ${({ newAnnotation }) => (newAnnotation ? "visible" : "hidden")}; +`; + +const When = styled.span` + width: 6.5em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + float: left; + + margin: 0 0.5em; + visibility: ${({ newAnnotation }) => (newAnnotation ? "visible" : "hidden")}; `; const LineNumber = styled.span` @@ -76,19 +93,10 @@ const LineNumber = styled.span` padding: 0 0.5em; `; -const When = styled.span` - width: 90px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - float: left; - - margin: 0 0.5em; -`; - const Annotate: FC = ({ source }) => { // @ts-ignore const defaultRenderer = ({ rows, stylesheet, useInlineStyles }) => { + let lastRevision = ""; // @ts-ignore return rows.map((node, i) => { const line = createElement({ @@ -100,10 +108,12 @@ const Annotate: FC = ({ source }) => { if (i + 1 < rows.length) { const annotation = source.lines[i]; + const newAnnotation = annotation.revision !== lastRevision; + lastRevision = annotation.revision; return ( - {annotation.author.name}{" "} - + {annotation.author.name}{" "} + {" "} {i + 1} {line} From 9c5e0c64fdf551f876935f4658aeaa528c9c6f14 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 12 Jun 2020 09:11:48 +0200 Subject: [PATCH 06/26] added missing license header --- scm-ui/ui-components/src/dates.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/scm-ui/ui-components/src/dates.ts b/scm-ui/ui-components/src/dates.ts index c335750ad2..f79fe25e63 100644 --- a/scm-ui/ui-components/src/dates.ts +++ b/scm-ui/ui-components/src/dates.ts @@ -1,3 +1,26 @@ +/* + * 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 { parseISO } from "date-fns"; From 24444aa06538ef08132c75b334e4e02fa61d1486 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 12 Jun 2020 11:29:23 +0200 Subject: [PATCH 07/26] implemented popover for annotate --- scm-ui/ui-components/src/Annotate.stories.tsx | 7 +- scm-ui/ui-components/src/Annotate.tsx | 95 +++++++++++++++++-- scm-ui/ui-components/src/DateFromNow.tsx | 9 +- .../src/repos/changesets/ChangesetAuthor.tsx | 11 ++- .../src/repos/changesets/index.ts | 2 +- 5 files changed, 110 insertions(+), 14 deletions(-) diff --git a/scm-ui/ui-components/src/Annotate.stories.tsx b/scm-ui/ui-components/src/Annotate.stories.tsx index 582c7545d0..d572a52bcd 100644 --- a/scm-ui/ui-components/src/Annotate.stories.tsx +++ b/scm-ui/ui-components/src/Annotate.stories.tsx @@ -26,6 +26,8 @@ import { storiesOf } from "@storybook/react"; import * as React from "react"; import styled from "styled-components"; import Annotate, { AnnotatedSource } from "./Annotate"; +import { MemoryRouter } from "react-router-dom"; +import repository from "./__resources__/repository"; const Wrapper = styled.div` margin: 2rem; @@ -108,5 +110,6 @@ const source: AnnotatedSource = { }; storiesOf("Annotate", module) - .addDecorator(storyFn => {storyFn()}) - .add("Default", () => ); + .addDecorator(storyFn => {storyFn()}) + .addDecorator(storyFn => {storyFn()}) + .add("Default", () => ); diff --git a/scm-ui/ui-components/src/Annotate.tsx b/scm-ui/ui-components/src/Annotate.tsx index e0e1768611..7ea1a85b43 100644 --- a/scm-ui/ui-components/src/Annotate.tsx +++ b/scm-ui/ui-components/src/Annotate.tsx @@ -23,7 +23,7 @@ */ import React, { FC } from "react"; -import { Person } from "@scm-manager/ui-types"; +import { Person, Repository } from "@scm-manager/ui-types"; // @ts-ignore import { LightAsync as ReactSyntaxHighlighter, createElement } from "react-syntax-highlighter"; @@ -32,6 +32,9 @@ import { LightAsync as ReactSyntaxHighlighter, createElement } from "react-synta import { arduinoLight } from "react-syntax-highlighter/dist/cjs/styles/hljs"; import styled from "styled-components"; import DateShort from "./DateShort"; +import { SingleContributor } from "./repos/changesets"; +import DateFromNow from "./DateFromNow"; +import { Link } from "react-router-dom"; // TODO move types to ui-types @@ -51,13 +54,14 @@ export type AnnotatedLine = { type Props = { source: AnnotatedSource; + repository: Repository; }; type LineElementProps = { newAnnotation: boolean; }; -const Author = styled.a` +const Author = styled.span` width: 8em; overflow: hidden; text-overflow: ellipsis; @@ -93,7 +97,72 @@ const LineNumber = styled.span` padding: 0 0.5em; `; -const Annotate: FC = ({ source }) => { +const Popover = styled.div` + position: absolute; + left: -16.5em; + bottom: 0.1em; + + z-index: 100; + visibility: hidden; + overflow: visible; + + width: 35em; + + &:before { + position: absolute; + content: ""; + border-style: solid; + pointer-events: none; + height: 0; + width: 0; + top: 100%; + /*left: 50%;*/ + border-color: transparent; + border-bottom-color: white; + border-left-color: white; + border-width: 0.4rem; + margin-left: -0.4rem; + margin-top: -0.4rem; + -webkit-transform-origin: center; + transform-origin: center; + box-shadow: -1px 1px 2px rgba(10, 10, 10, 0.2); + transform: rotate(-45deg); + } +`; + +const Line = styled.span` + position: relative; + z-index: 10; + + &:hover .changeset-details { + visibility: visible !important; + } +`; + +const PreTag = styled.pre` + overflow-x: visible !important; +`; + +const SmallHr = styled.hr` + margin: 0.5em 0; +`; + +const PopoverHeading = styled.div` + height: 1.5em; +`; + +const PopoverDescription = styled.p` + margin-top: 0.5em; +`; + +const shortRevision = (revision: string) => { + if (revision.length > 7) { + return revision.substring(0, 7); + } + return revision; +}; + +const Annotate: FC = ({ source, repository }) => { // @ts-ignore const defaultRenderer = ({ rows, stylesheet, useInlineStyles }) => { let lastRevision = ""; @@ -111,13 +180,26 @@ const Annotate: FC = ({ source }) => { const newAnnotation = annotation.revision !== lastRevision; lastRevision = annotation.revision; return ( - - {annotation.author.name}{" "} + + + + + + + +

Changeset {shortRevision(annotation.revision)}

+ {annotation.description} +
+ + + {annotation.author.name} + + {" "} {" "} {i + 1} {line} -
+ ); } @@ -136,6 +218,7 @@ const Annotate: FC = ({ source }) => { language={source.language} style={arduinoLight} renderer={defaultRenderer} + PreTag={PreTag} > {code} diff --git a/scm-ui/ui-components/src/DateFromNow.tsx b/scm-ui/ui-components/src/DateFromNow.tsx index eda6efdcc7..b01cfcdc7b 100644 --- a/scm-ui/ui-components/src/DateFromNow.tsx +++ b/scm-ui/ui-components/src/DateFromNow.tsx @@ -49,6 +49,7 @@ type Props = WithTranslation & { * ci server. */ baseDate?: DateInput; + className?: string; }; type Options = { @@ -100,13 +101,17 @@ class DateFromNow extends React.Component { }; render() { - const { date } = this.props; + const { date, className } = this.props; if (date) { const isoDate = toDate(date); const options = this.createOptions(); const distance = formatDistance(isoDate, this.getBaseDate(), options); const formatted = format(isoDate, FullDateFormat, options); - return {distance}; + return ( + + {distance} + + ); } return null; } diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx index 017d468888..2af17643bc 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetAuthor.tsx @@ -36,6 +36,7 @@ type Props = { type PersonProps = { person: Person; + className?: string; displayTextOnly?: boolean; }; @@ -70,7 +71,7 @@ const ContributorWithAvatar: FC = ({ person, avatar }) => { return ; }; -const SingleContributor: FC = ({ person, displayTextOnly }) => { +export const SingleContributor: FC = ({ person, className, displayTextOnly }) => { const [t] = useTranslation("repos"); const avatar = useAvatar(person); if (!displayTextOnly && avatar) { @@ -78,12 +79,16 @@ const SingleContributor: FC = ({ person, displayTextOnly }) => { } if (person.mail) { return ( - + {person.name} ); } - return <>{person.name}; + return {person.name}; }; type PersonsProps = { diff --git a/scm-ui/ui-components/src/repos/changesets/index.ts b/scm-ui/ui-components/src/repos/changesets/index.ts index 68e483cde8..b4c83dd4c3 100644 --- a/scm-ui/ui-components/src/repos/changesets/index.ts +++ b/scm-ui/ui-components/src/repos/changesets/index.ts @@ -25,7 +25,7 @@ import * as changesets from "./changesets"; export { changesets }; -export { default as ChangesetAuthor } from "./ChangesetAuthor"; +export { default as ChangesetAuthor, SingleContributor } from "./ChangesetAuthor"; export { default as ChangesetButtonGroup } from "./ChangesetButtonGroup"; export { default as ChangesetDiff } from "./ChangesetDiff"; export { default as ChangesetId } from "./ChangesetId"; From becb0d02082e79c68011a78390b0b80ddc7ac216 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 12 Jun 2020 13:19:48 +0200 Subject: [PATCH 08/26] use fixed dates for story --- scm-ui/ui-components/src/Annotate.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scm-ui/ui-components/src/Annotate.stories.tsx b/scm-ui/ui-components/src/Annotate.stories.tsx index d572a52bcd..aae6a2036a 100644 --- a/scm-ui/ui-components/src/Annotate.stories.tsx +++ b/scm-ui/ui-components/src/Annotate.stories.tsx @@ -40,7 +40,7 @@ const commitCreateNewApp = { mail: "arthur.dent@hitchhiker.com" }, description: "create new app", - when: new Date() + when: new Date("2020-04-09T13:07:42Z") }; const commitFixedMissingImport = { @@ -50,7 +50,7 @@ const commitFixedMissingImport = { mail: "trillian@hitchhiker.com" }, description: "fixed missing import", - when: new Date() + when: new Date("2020-05-10T09:18:42Z") }; const commitImplementMain = { @@ -60,7 +60,7 @@ const commitImplementMain = { mail: "ford.prefect@hitchhiker.com" }, description: "implemented main function", - when: new Date() + when: new Date("2020-04-12T16:29:42Z") }; const source: AnnotatedSource = { From 2fab4f5eea906b6ca4492e98ad4e613995151b44 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 12 Jun 2020 13:20:04 +0200 Subject: [PATCH 09/26] reduce ts-ignore count --- scm-ui/ui-components/src/Annotate.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/scm-ui/ui-components/src/Annotate.tsx b/scm-ui/ui-components/src/Annotate.tsx index 7ea1a85b43..31dd10e327 100644 --- a/scm-ui/ui-components/src/Annotate.tsx +++ b/scm-ui/ui-components/src/Annotate.tsx @@ -27,8 +27,6 @@ import { Person, Repository } from "@scm-manager/ui-types"; // @ts-ignore import { LightAsync as ReactSyntaxHighlighter, createElement } from "react-syntax-highlighter"; - -// @ts-ignore import { arduinoLight } from "react-syntax-highlighter/dist/cjs/styles/hljs"; import styled from "styled-components"; import DateShort from "./DateShort"; @@ -116,7 +114,7 @@ const Popover = styled.div` height: 0; width: 0; top: 100%; - /*left: 50%;*/ + left: 5.5em; border-color: transparent; border-bottom-color: white; border-left-color: white; @@ -163,11 +161,9 @@ const shortRevision = (revision: string) => { }; const Annotate: FC = ({ source, repository }) => { - // @ts-ignore - const defaultRenderer = ({ rows, stylesheet, useInlineStyles }) => { + const defaultRenderer = ({ rows, stylesheet, useInlineStyles }: any) => { let lastRevision = ""; - // @ts-ignore - return rows.map((node, i) => { + return rows.map((node: any, i: number) => { const line = createElement({ node, stylesheet, From 92d7b7703af1cd3527f6a601676725f79d1bdecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 12 Jun 2020 17:27:32 +0200 Subject: [PATCH 10/26] Add "annotations" button to sources view --- .../components/content/FileButtonAddons.tsx | 28 ++++++-- .../repos/sources/containers/AnnotateView.tsx | 70 +++++++++++++++++++ .../src/repos/sources/containers/Content.tsx | 48 +++++++------ .../sonia/scm/api/v2/resources/BlameDto.java | 2 +- .../BlameResultToBlameDtoMapper.java | 2 +- 5 files changed, 121 insertions(+), 29 deletions(-) create mode 100644 scm-ui/ui-webapp/src/repos/sources/containers/AnnotateView.tsx diff --git a/scm-ui/ui-webapp/src/repos/sources/components/content/FileButtonAddons.tsx b/scm-ui/ui-webapp/src/repos/sources/components/content/FileButtonAddons.tsx index 38f803e958..f2dfe8a7b5 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/content/FileButtonAddons.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/content/FileButtonAddons.tsx @@ -24,20 +24,27 @@ import React from "react"; import { WithTranslation, withTranslation } from "react-i18next"; import { Button, ButtonAddons } from "@scm-manager/ui-components"; +import { SourceViewSelection } from "../../containers/Content"; type Props = WithTranslation & { className?: string; - historyIsSelected: boolean; - showHistory: (p: boolean) => void; + selected: SourceViewSelection; + showSources: () => void; + showHistory: () => void; + showAnnotations: () => void; }; class FileButtonAddons extends React.Component { showHistory = () => { - this.props.showHistory(true); + this.props.showHistory(); }; showSources = () => { - this.props.showHistory(false); + this.props.showSources(); + }; + + showAnnotations = () => { + this.props.showAnnotations(); }; color = (selected: boolean) => { @@ -45,19 +52,26 @@ class FileButtonAddons extends React.Component { }; render() { - const { className, t, historyIsSelected } = this.props; + const { className, t, selected, showSources, showHistory, showAnnotations } = this.props; return (
-
+
+ +
-