From 6d409c65c0a9f7a3db524ac550217abf483dc564 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Tue, 25 Aug 2020 14:45:48 +0200 Subject: [PATCH] initial implementation --- .../main/java/sonia/scm/repository/Tag.java | 15 ++++ .../java/sonia/scm/repository/GitUtil.java | 40 +++++++++++ .../repository/api/GitHookTagProvider.java | 61 +++++++++------- .../spi/GitHookContextProvider.java | 2 +- .../scm/repository/spi/GitTagsCommand.java | 65 +++++++---------- .../repository/client/spi/GitTagCommand.java | 6 +- .../repository/spi/GitTagsCommandTest.java | 67 ++++++++++++++++++ .../repository/spi/scm-git-spi-test-tags.zip | Bin 0 -> 44688 bytes .../scm/repository/spi/HgTagsCommand.java | 43 ++++------- .../scm/repository/spi/HgTagsCommandTest.java | 45 ++++++++++++ .../DefaultChangesetToChangesetDtoMapper.java | 19 ++++- .../sonia/scm/api/v2/resources/TagDto.java | 6 +- .../api/v2/resources/TagToTagDtoMapper.java | 9 +++ .../v2/resources/TagToTagDtoMapperTest.java | 8 +++ 14 files changed, 284 insertions(+), 102 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagsCommandTest.java create mode 100644 scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test-tags.zip create mode 100644 scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgTagsCommandTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/Tag.java b/scm-core/src/main/java/sonia/scm/repository/Tag.java index 3c40c2f38a..bdeec544a4 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Tag.java +++ b/scm-core/src/main/java/sonia/scm/repository/Tag.java @@ -41,6 +41,7 @@ public final class Tag { private final String name; private final String revision; + private final Long date; /** * Constructs a new tag. @@ -49,7 +50,21 @@ public final class Tag { * @param revision tagged revision */ public Tag(String name, String revision) { + this(name, revision, null); + } + + /** + * Constructs a new tag. + * + * @param name name of the tag + * @param revision tagged revision + * @param date the creation timestamp (milliseconds) of the tag + * + * @since 2.5.0 + */ + public Tag(String name, String revision, Long date) { this.name = name; this.revision = revision; + this.date = date; } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index b55aec06bd..96e4266a5d 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -42,6 +42,8 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; @@ -387,6 +389,44 @@ public final class GitUtil return ref; } + /** + * Method description + * + * + * @param repository + * @param revWalk + * @param ref + * + * @return + * + * @throws IOException + * + * @since 2.5.0 + */ + public static Long getTagTime(org.eclipse.jgit.lib.Repository repository, + RevWalk revWalk, Ref ref) + throws IOException + { + ObjectId id = ref.getObjectId(); + + if (id != null) + { + if (revWalk == null) + { + revWalk = new RevWalk(repository); + } + + final RevObject revObject = revWalk.parseAny(id); + if (revObject instanceof RevTag) { + return ((RevTag) revObject).getTaggerIdent().getWhen().getTime(); + } else if (revObject instanceof RevCommit) { + return getCommitTime((RevCommit) revObject); + } + } + + return null; + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java index 2a290c6ca9..5bc5069112 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java @@ -21,12 +21,17 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; + +import java.io.IOException; import java.util.List; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,52 +40,60 @@ import sonia.scm.repository.Tag; /** * Git provider implementation of {@link HookTagProvider}. - * - * @since 1.50 + * * @author Sebastian Sdorra + * @since 1.50 */ public class GitHookTagProvider implements HookTagProvider { - private static final Logger logger = LoggerFactory.getLogger(GitHookTagProvider.class); - + private static final Logger LOG = LoggerFactory.getLogger(GitHookTagProvider.class); + private final List createdTags; private final List deletedTags; /** * Constructs new instance. - * + * * @param commands received commands */ - public GitHookTagProvider(List commands) { + public GitHookTagProvider(List commands, Repository repository) { ImmutableList.Builder createdTagBuilder = ImmutableList.builder(); ImmutableList.Builder deletedTagBuilder = ImmutableList.builder(); - - for ( ReceiveCommand rc : commands ){ + + for (ReceiveCommand rc : commands) { String refName = rc.getRefName(); String tag = GitUtil.getTagName(refName); - - if (Strings.isNullOrEmpty(tag)){ - logger.debug("received ref name {} is not a tag", refName); - } else if (isCreate(rc)) { - createdTagBuilder.add(createTagFromNewId(rc, tag)); - } else if (isDelete(rc)){ - deletedTagBuilder.add(createTagFromOldId(rc, tag)); - } else if (isUpdate(rc)) { - createdTagBuilder.add(createTagFromNewId(rc, tag)); - deletedTagBuilder.add(createTagFromOldId(rc, tag)); + + if (Strings.isNullOrEmpty(tag)) { + LOG.debug("received ref name {} is not a tag", refName); + } else { + Long tagTime = null; + try (RevWalk walk = new RevWalk(repository)) { + tagTime = GitUtil.getTagTime(repository, walk, rc.getRef()); + } catch (IOException e) { + LOG.error("Could not read tag time", e); + } + if (isCreate(rc)) { + createdTagBuilder.add(createTagFromNewId(rc, tag, tagTime)); + } else if (isDelete(rc)) { + deletedTagBuilder.add(createTagFromOldId(rc, tag, tagTime)); + } else if (isUpdate(rc)) { + createdTagBuilder.add(createTagFromNewId(rc, tag, tagTime)); + deletedTagBuilder.add(createTagFromOldId(rc, tag, tagTime)); + } } } - + createdTags = createdTagBuilder.build(); deletedTags = deletedTagBuilder.build(); } - private Tag createTagFromNewId(ReceiveCommand rc, String tag) { - return new Tag(tag, GitUtil.getId(rc.getNewId())); + private Tag createTagFromNewId(ReceiveCommand rc, String tag, Long tagTime) { + return new Tag(tag, GitUtil.getId(rc.getNewId()), tagTime); } - private Tag createTagFromOldId(ReceiveCommand rc, String tag) { - return new Tag(tag, GitUtil.getId(rc.getOldId())); + private Tag createTagFromOldId(ReceiveCommand rc, String tag, Long tagTime) { + return new Tag(tag, GitUtil.getId(rc.getOldId()), tagTime); } private boolean isUpdate(ReceiveCommand rc) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java index 13a11007a2..ce61b85a57 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHookContextProvider.java @@ -103,7 +103,7 @@ public class GitHookContextProvider extends HookContextProvider @Override public HookTagProvider getTagProvider() { - return new GitHookTagProvider(receiveCommands); + return new GitHookTagProvider(receiveCommands, repository); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java index 208c931f63..97b9c91be1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagsCommand.java @@ -31,7 +31,7 @@ import com.google.common.collect.Lists; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,34 +45,28 @@ import java.util.List; //~--- JDK imports ------------------------------------------------------------ /** - * * @author Sebastian Sdorra */ -public class GitTagsCommand extends AbstractGitCommand implements TagsCommand -{ +public class GitTagsCommand extends AbstractGitCommand implements TagsCommand { /** * Constructs ... * - * @param context - * + * @param context */ - public GitTagsCommand(GitContext context) - { + public GitTagsCommand(GitContext context) { super(context); } //~--- get methods ---------------------------------------------------------- @Override - public List getTags() throws IOException - { + public List getTags() throws IOException { List tags = null; RevWalk revWalk = null; - try - { + try { final Git git = new Git(open()); revWalk = new RevWalk(git.getRepository()); @@ -81,13 +75,9 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand tags = Lists.transform(tagList, new TransformFuntion(git.getRepository(), revWalk)); - } - catch (GitAPIException ex) - { + } catch (GitAPIException ex) { throw new InternalRepositoryException(repository, "could not read tags from repository", ex); - } - finally - { + } finally { GitUtil.release(revWalk); } @@ -99,12 +89,10 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand /** * Class description * - * - * @version Enter version here..., 12/07/06 - * @author Enter your name here... + * @author Enter your name here... + * @version Enter version here..., 12/07/06 */ - private static class TransformFuntion implements Function - { + private static class TransformFuntion implements Function { /** * the logger for TransformFuntion @@ -117,13 +105,11 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand /** * Constructs ... * - * * @param repository * @param revWalk */ public TransformFuntion(org.eclipse.jgit.lib.Repository repository, - RevWalk revWalk) - { + RevWalk revWalk) { this.repository = repository; this.revWalk = revWalk; } @@ -133,30 +119,23 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand /** * Method description * - * * @param ref - * * @return */ @Override - public Tag apply(Ref ref) - { + public Tag apply(Ref ref) { Tag tag = null; - try - { - RevCommit commit = GitUtil.getCommit(repository, revWalk, ref); + try { + RevObject revObject = revWalk.parseAny(ref.getObjectId()); - if (commit != null) - { + if (revObject != null) { String name = GitUtil.getTagName(ref); - tag = new Tag(name, commit.getId().name()); + tag = new Tag(name, revObject.getId().name(), GitUtil.getTagTime(repository, revWalk, ref)); } - } - catch (IOException ex) - { + } catch (IOException ex) { logger.error("could not get commit for tag", ex); } @@ -165,10 +144,14 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand //~--- fields ------------------------------------------------------------- - /** Field description */ + /** + * Field description + */ private org.eclipse.jgit.lib.Repository repository; - /** Field description */ + /** + * Field description + */ private RevWalk revWalk; } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java index db44ee5a90..19b93d0775 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java @@ -78,6 +78,7 @@ public class GitTagCommand implements TagCommand String revision = request.getRevision(); RevObject revObject = null; + Long tagTime = null; if (!Strings.isNullOrEmpty(revision)) { @@ -88,6 +89,7 @@ public class GitTagCommand implements TagCommand { walk = new RevWalk(git.getRepository()); revObject = walk.parseAny(id); + tagTime = GitUtil.getTagTime(git.getRepository(), walk, GitUtil.getRefForCommit(git.getRepository(), id)); } finally { @@ -110,9 +112,9 @@ public class GitTagCommand implements TagCommand } if (ref.isPeeled()) { - tag = new Tag(request.getName(), ref.getPeeledObjectId().toString()); + tag = new Tag(request.getName(), ref.getPeeledObjectId().toString(), tagTime); } else { - tag = new Tag(request.getName(), ref.getObjectId().toString()); + tag = new Tag(request.getName(), ref.getObjectId().toString(), tagTime); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagsCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagsCommandTest.java new file mode 100644 index 0000000000..afa3cc4930 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagsCommandTest.java @@ -0,0 +1,67 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.repository.spi; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.Tag; + +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") +public class GitTagsCommandTest extends AbstractGitCommandTestBase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Test + public void shouldGetDatesCorrectly() throws IOException { + final GitContext gitContext = createContext(); + final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext); + final List tags = tagsCommand.getTags(); + assertThat(tags).hasSize(2); + assertThat(tags.get(0).getName()).isEqualTo("1.0.0"); + assertThat(tags.get(0).getDate()).isEqualTo(1598348105000L); // Annotated - Take tag date + assertThat(tags.get(1).getName()).isEqualTo("test-tag"); + assertThat(tags.get(1).getDate()).isEqualTo(1339416344000L); // Lightweight - Take commit date + } + + @Override + protected String getZippedRepositoryResource() { + return "sonia/scm/repository/spi/scm-git-spi-test-tags.zip"; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test-tags.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test-tags.zip new file mode 100644 index 0000000000000000000000000000000000000000..05b2f0f7cab59ed00cab488ed71e36611e5f6983 GIT binary patch literal 44688 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 Xyu6rL`jhSIzuVO5qDRyp`F#HeSg)q) literal 0 HcmV?d00001 diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagsCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagsCommand.java index 309265617c..b0fb323bab 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagsCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagsCommand.java @@ -29,29 +29,22 @@ package sonia.scm.repository.spi; import com.google.common.base.Function; import com.google.common.base.Strings; import com.google.common.collect.Lists; - import sonia.scm.repository.Tag; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.util.List; /** - * * @author Sebastian Sdorra */ -public class HgTagsCommand extends AbstractCommand implements TagsCommand -{ +public class HgTagsCommand extends AbstractCommand implements TagsCommand { /** * Constructs ... * - * @param context - * + * @param context */ - public HgTagsCommand(HgCommandContext context) - { + public HgTagsCommand(HgCommandContext context) { super(context); } @@ -60,12 +53,10 @@ public class HgTagsCommand extends AbstractCommand implements TagsCommand /** * Method description * - * * @return */ @Override - public List getTags() - { + public List getTags() { com.aragost.javahg.commands.TagsCommand cmd = com.aragost.javahg.commands.TagsCommand.on(open()); @@ -74,13 +65,11 @@ public class HgTagsCommand extends AbstractCommand implements TagsCommand List tags = null; // check for empty repository - if (Util.isNotEmpty(tagList) && tagList.get(0).getChangeset() != null) - { + if (Util.isNotEmpty(tagList) && tagList.get(0).getChangeset() != null) { tags = Lists.transform(tagList, new TagTransformer()); } - if (tags == null) - { + if (tags == null) { tags = Lists.newArrayList(); } @@ -92,31 +81,25 @@ public class HgTagsCommand extends AbstractCommand implements TagsCommand /** * Class description * - * - * @version Enter version here..., 12/08/03 - * @author Enter your name here... + * @author Enter your name here... + * @version Enter version here..., 12/08/03 */ private static class TagTransformer - implements Function - { + implements Function { /** * Method description * - * * @param f - * * @return */ @Override - public Tag apply(com.aragost.javahg.Tag f) - { + public Tag apply(com.aragost.javahg.Tag f) { Tag t = null; - if ((f != null) &&!Strings.isNullOrEmpty(f.getName()) - && (f.getChangeset() != null)) - { - t = new Tag(f.getName(), f.getChangeset().getNode()); + if ((f != null) && !Strings.isNullOrEmpty(f.getName()) + && (f.getChangeset() != null)) { + t = new Tag(f.getName(), f.getChangeset().getNode(), f.getChangeset().getTimestamp().getDate().getTime()); } return t; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgTagsCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgTagsCommandTest.java new file mode 100644 index 0000000000..f9d3fdd5b2 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgTagsCommandTest.java @@ -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. + */ + +package sonia.scm.repository.spi; + +import org.junit.Test; +import sonia.scm.repository.Tag; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HgTagsCommandTest extends AbstractHgCommandTestBase { + + @Test + public void shouldGetTagDatesCorrectly() { + HgTagsCommand hgTagsCommand = new HgTagsCommand(cmdContext); + final List tags = hgTagsCommand.getTags(); + assertThat(tags).hasSize(1); + assertThat(tags.get(0).getName()).isEqualTo("tip"); + assertThat(tags.get(0).getDate()).isEqualTo(1339586381000L); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java index 384331c6ad..c05547b295 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java @@ -29,13 +29,15 @@ import de.otto.edison.hal.Links; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.ObjectFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.repository.Branch; import sonia.scm.repository.Changeset; import sonia.scm.repository.Contributor; import sonia.scm.repository.Person; import sonia.scm.repository.Repository; import sonia.scm.repository.Signature; -import sonia.scm.repository.Tag; +import sonia.scm.repository.Tags; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -45,6 +47,7 @@ import sonia.scm.security.gpg.RawGpgKey; import sonia.scm.web.EdisonHalAppender; import javax.inject.Inject; +import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -57,6 +60,8 @@ import static de.otto.edison.hal.Links.linkingTo; @Mapper public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMapper implements InstantAttributeMapper, ChangesetToChangesetDtoMapper { + private static Logger LOG = LoggerFactory.getLogger(DefaultChangesetToChangesetDtoMapper.class); + @Inject private RepositoryServiceFactory serviceFactory; @@ -115,8 +120,16 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa try (RepositoryService repositoryService = serviceFactory.create(repository)) { if (repositoryService.isSupported(Command.TAGS)) { - embeddedBuilder.with("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name, - getListOfObjects(source.getTags(), tagName -> new Tag(tagName, source.getId())))); + Tags tags = null; + try { + tags = repositoryService.getTagsCommand().getTags(); + } catch (IOException e) { + LOG.error("Error while retrieving tags from repository", e); + } + if (tags != null) { + embeddedBuilder.with("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name, + getListOfObjects(source.getTags(), tags::getTagByName))); + } } if (repositoryService.isSupported(Command.BRANCHES)) { embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name, diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java index f96417a6bd..b7c6b3bacf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.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 de.otto.edison.hal.Embedded; @@ -31,6 +31,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.time.Instant; + @Getter @Setter @NoArgsConstructor @@ -40,6 +42,8 @@ public class TagDto extends HalRepresentation { private String revision; + private Instant date; + TagDto(Links links, Embedded embedded) { super(links, embedded); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java index e056162dcc..532eb9be85 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java @@ -29,6 +29,7 @@ import de.otto.edison.hal.Links; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.Named; import org.mapstruct.ObjectFactory; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Tag; @@ -36,6 +37,8 @@ import sonia.scm.web.EdisonHalAppender; import javax.inject.Inject; +import java.time.Instant; + import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @@ -46,6 +49,7 @@ public abstract class TagToTagDtoMapper extends HalAppenderMapper { @Inject private ResourceLinks resourceLinks; + @Mapping(target = "date", source = "date", qualifiedByName = "mapDate") @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract TagDto map(Tag tag, @Context NamespaceAndName namespaceAndName); @@ -61,4 +65,9 @@ public abstract class TagToTagDtoMapper extends HalAppenderMapper { return new TagDto(linksBuilder.build(), embeddedBuilder.build()); } + + @Named("mapDate") + Instant map(Long value) { + return value == null ? null : Instant.ofEpochMilli(value); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java index f53a10797b..ebf71f6b85 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java @@ -32,6 +32,7 @@ import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Tag; import java.net.URI; +import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; @@ -58,4 +59,11 @@ class TagToTagDtoMapperTest { assertThat(dto.getLinks().getLinkBy("yo").get().getHref()).isEqualTo("http://hitchhiker/hog/1.0.0"); } + @Test + void shouldMapDate() { + final long now = Instant.now().getEpochSecond() * 1000; + TagDto dto = mapper.map(new Tag("1.0.0", "42", now), new NamespaceAndName("hitchhiker", "hog")); + assertThat(dto.getDate()).isEqualTo(Instant.ofEpochMilli(now)); + } + }