diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e314ea7e..d4e859c4ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- Tags now have date information attached ([#1305](https://github.com/scm-manager/scm-manager/pull/1305)) - Add support for scroll anchors in url hash of diff page ([#1304](https://github.com/scm-manager/scm-manager/pull/1304)) ### Fixed diff --git a/pom.xml b/pom.xml index f78accff67..280c1e73ee 100644 --- a/pom.xml +++ b/pom.xml @@ -903,7 +903,7 @@ - 2.28.2 + 3.5.6 2.1 5.6.2 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..cdfac1c0e6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Tag.java +++ b/scm-core/src/main/java/sonia/scm/repository/Tag.java @@ -28,6 +28,8 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; +import java.util.Optional; + /** * Represents a tag in a repository. * @@ -41,6 +43,7 @@ public final class Tag { private final String name; private final String revision; + private final Long date; /** * Constructs a new tag. @@ -49,7 +52,41 @@ 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; + } + + /** + * Depending on the underlying source code management system + * (like git or hg) and depending on the type of this tag + * (for example git has lightweight and annotated + * tags), this date has different meaning. For annotated tags + * in git, this is the date the tag was created. In other cases + * (for lightweight tags in git or all tags in hg) this is the + * date of the referenced changeset. + *

+ * Please note, that the date is retrieved in a best-effort fashion. + * In certain situations (for example if this tag is announced in + * a pre or post receive hook), it might not be available. + * In these cases, this method returns an empty {@link Optional}. + * + * @since 2.5.0 + */ + public Optional getDate() { + return Optional.ofNullable(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..6baa48111b 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,31 @@ public final class GitUtil return ref; } + /** + * @since 2.5.0 + */ + public static Long getTagTime(org.eclipse.jgit.lib.Repository repository, ObjectId objectId) throws IOException { + try (RevWalk walk = new RevWalk(repository)) { + return GitUtil.getTagTime(walk, objectId); + } + } + + /** + * @since 2.5.0 + */ + public static Long getTagTime(RevWalk revWalk, ObjectId objectId) throws IOException { + if (objectId != null) { + final RevObject revObject = revWalk.parseAny(objectId); + 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..7889badc30 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,66 +21,75 @@ * 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.util.List; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceiveCommand; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitUtil; import sonia.scm.repository.Tag; +import java.io.IOException; +import java.util.List; + /** * 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 { + try { + if (isCreate(rc)) { + createdTagBuilder.add(createTagFromNewId(rc, tag, GitUtil.getTagTime(repository, rc.getNewId()))); + } else if (isDelete(rc)) { + deletedTagBuilder.add(createTagFromOldId(rc, tag, GitUtil.getTagTime(repository, rc.getOldId()))); + } else if (isUpdate(rc)) { + createdTagBuilder.add(createTagFromNewId(rc, tag, GitUtil.getTagTime(repository, rc.getNewId()))); + deletedTagBuilder.add(createTagFromOldId(rc, tag, GitUtil.getTagTime(repository, rc.getOldId()))); + } + } catch (IOException e) { + LOG.error("Could not read tag time", e); + } } } - + 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..60b1105547 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 = GitUtil.getCommit(repository, revWalk, ref); - 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(revWalk, ref.getObjectId())); } - } - 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/api/GitHookTagProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java index d27f881a13..ccf21b83c9 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/api/GitHookTagProviderTest.java @@ -21,30 +21,35 @@ * 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.collect.Lists; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceiveCommand; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.GitUtil; import sonia.scm.repository.Tag; import java.util.List; import static org.hamcrest.Matchers.empty; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; /** * Unit tests for {@link GitHookTagProvider}. - * + * * @author Sebastian Sdorra */ @RunWith(MockitoJUnitRunner.class) @@ -52,19 +57,19 @@ public class GitHookTagProviderTest { private static final String ZERO = ObjectId.zeroId().getName(); - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Mock private ReceiveCommand command; - + + @Mock + private Repository repository; + private List commands; - + /** * Set up mocks for upcoming tests. */ @Before - public void setUpMocks(){ + public void setUpMocks() { commands = Lists.newArrayList(command); } @@ -73,65 +78,125 @@ public class GitHookTagProviderTest { */ @Test public void testGetCreatedTags() { - String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; - GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/tags/1.0.0", revision, ZERO); - - assertTag("1.0.0", revision, provider.getCreatedTags()); - assertThat(provider.getDeletedTags(), empty()); + try (MockedStatic dummy = Mockito.mockStatic(GitUtil.class)) { + String revision = "86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1"; + Long timestamp = 1339416344000L; + String tagName = "1.0.0"; + String ref = "refs/tags/" + tagName; + + dummy.when(() -> GitUtil.getTagTime(repository, ObjectId.fromString(revision))).thenReturn(timestamp); + dummy.when(() -> GitUtil.getTagName(ref)).thenReturn(tagName); + dummy.when(() -> GitUtil.getId(ObjectId.fromString(revision))).thenReturn(revision); + + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, ref, revision, ZERO); + + assertTag(tagName, revision, timestamp, provider.getCreatedTags()); + assertThat(provider.getDeletedTags(), empty()); + } } - + /** * Tests {@link GitHookTagProvider#getDeletedTags()}. */ @Test public void testGetDeletedTags() { - String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; - GitHookTagProvider provider = createProvider(ReceiveCommand.Type.DELETE, "refs/tags/1.0.0", ZERO, revision); - - assertThat(provider.getCreatedTags(), empty()); - assertTag("1.0.0", revision, provider.getDeletedTags()); + try (MockedStatic dummy = Mockito.mockStatic(GitUtil.class)) { + String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; + Long timestamp = 1339416344000L; + String tagName = "1.0.0"; + String ref = "refs/tags/" + tagName; + + dummy.when(() -> GitUtil.getTagTime(repository, ObjectId.fromString(revision))).thenReturn(timestamp); + dummy.when(() -> GitUtil.getTagName(ref)).thenReturn(tagName); + dummy.when(() -> GitUtil.getId(ObjectId.fromString(revision))).thenReturn(revision); + + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.DELETE, ref, ZERO, revision); + + assertThat(provider.getCreatedTags(), empty()); + assertTag("1.0.0", revision, 1339416344000L, provider.getDeletedTags()); + } } - + /** * Tests {@link GitHookTagProvider} with a branch ref instead of a tag. */ @Test - public void testWithBranch(){ + public void testWithBranch() { String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/heads/1.0.0", revision, revision); - + assertThat(provider.getCreatedTags(), empty()); assertThat(provider.getDeletedTags(), empty()); } /** - * Tests {@link GitHookTagProvider} with update command. + * Tests {@link GitHookTagProvider} with update command pre receive. */ @Test - public void testUpdateTags() { - String newId = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; - String oldId = "e0f2be968b147ff7043684a7715d2fe852553db4"; + public void testUpdateTagsPreReceive() { + try (MockedStatic dummy = Mockito.mockStatic(GitUtil.class)) { + String oldRevision = "e0f2be968b147ff7043684a7715d2fe852553db4"; + String newRevision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; - GitHookTagProvider provider = createProvider(ReceiveCommand.Type.UPDATE, "refs/tags/1.0.0", newId, oldId); - assertTag("1.0.0", newId, provider.getCreatedTags()); - assertTag("1.0.0", oldId, provider.getDeletedTags()); + Long timestamp = 1339416344000L; + String tagName = "1.0.0"; + String ref = "refs/tags/" + tagName; + + dummy.when(() -> GitUtil.getTagTime(repository, ObjectId.fromString(oldRevision))).thenReturn(timestamp); + dummy.when(() -> GitUtil.getTagTime(repository, ObjectId.fromString(newRevision))).thenReturn(null); + dummy.when(() -> GitUtil.getTagName(ref)).thenReturn(tagName); + dummy.when(() -> GitUtil.getId(ObjectId.fromString(oldRevision))).thenReturn(oldRevision); + dummy.when(() -> GitUtil.getId(ObjectId.fromString(newRevision))).thenReturn(newRevision); + + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.UPDATE, ref, newRevision, oldRevision); + + assertTag(tagName, newRevision, null, provider.getCreatedTags()); + assertTag(tagName, oldRevision, timestamp, provider.getDeletedTags()); + } } - private void assertTag(String name, String revision, List tags){ + /** + * Tests {@link GitHookTagProvider} with update command post receive. + */ + @Test + public void testUpdateTagsPostReceive() { + try (MockedStatic dummy = Mockito.mockStatic(GitUtil.class)) { + String oldRevision = "e0f2be968b147ff7043684a7715d2fe852553db4"; + String newRevision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; + + Long timestamp = 1339416344000L; + String tagName = "1.0.0"; + String ref = "refs/tags/" + tagName; + + dummy.when(() -> GitUtil.getTagTime(repository, ObjectId.fromString(newRevision))).thenReturn(timestamp); + dummy.when(() -> GitUtil.getTagTime(repository, ObjectId.fromString(oldRevision))).thenReturn(null); + dummy.when(() -> GitUtil.getTagName(ref)).thenReturn(tagName); + dummy.when(() -> GitUtil.getId(ObjectId.fromString(oldRevision))).thenReturn(oldRevision); + dummy.when(() -> GitUtil.getId(ObjectId.fromString(newRevision))).thenReturn(newRevision); + + GitHookTagProvider provider = createProvider(ReceiveCommand.Type.UPDATE, ref, newRevision, oldRevision); + + assertTag(tagName, newRevision, timestamp, provider.getCreatedTags()); + assertTag(tagName, oldRevision, null, provider.getDeletedTags()); + } + } + + private void assertTag(String name, String revision, Long date, List tags) { assertNotNull(tags); assertFalse(tags.isEmpty()); assertEquals(1, tags.size()); Tag tag = tags.get(0); assertEquals(name, tag.getName()); assertEquals(revision, tag.getRevision()); + assertEquals(date, tag.getDate().orElse(null)); } - - private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String newId, String oldId){ + + private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String newId, String oldId) { when(command.getNewId()).thenReturn(ObjectId.fromString(newId)); when(command.getOldId()).thenReturn(ObjectId.fromString(oldId)); when(command.getType()).thenReturn(type); when(command.getRefName()).thenReturn(ref); - return new GitHookTagProvider(commands); + return new GitHookTagProvider(commands, repository); } } 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..8c4eb3f89f 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 @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.client.spi; //~--- non-JDK imports -------------------------------------------------------- @@ -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(walk, 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..9112aa3133 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagsCommandTest.java @@ -0,0 +1,73 @@ +/* + * 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); + + Tag annotatedTag = tags.get(0); + assertThat(annotatedTag.getName()).isEqualTo("1.0.0"); + assertThat(annotatedTag.getDate()).contains(1598348105000L); // Annotated - Take tag date + assertThat(annotatedTag.getRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + + Tag lightweightTag = tags.get(1); + assertThat(lightweightTag.getName()).isEqualTo("test-tag"); + assertThat(lightweightTag.getDate()).contains(1339416344000L); // Lightweight - Take commit date + assertThat(lightweightTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1"); + } + + @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 0000000000..05b2f0f7ca Binary files /dev/null and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test-tags.zip differ diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookTagProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookTagProvider.java index ed140ee4b6..7b8ecef67d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookTagProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/api/HgHookTagProvider.java @@ -92,7 +92,7 @@ public class HgHookTagProvider implements HookTagProvider { if (tagNames != null){ for ( String tagName : tagNames ){ logger.trace("found tag {} at changeset {}", tagName, c.getId()); - tags.add(new Tag(tagName, c.getId())); + tags.add(new Tag(tagName, c.getId(), c.getDate())); } } } 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/api/HgHookTagProviderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java index a2486732a0..b756b4097d 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/api/HgHookTagProviderTest.java @@ -21,15 +21,11 @@ * 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.collect.Lists; -import java.util.List; import org.junit.Test; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; -import static org.hamcrest.Matchers.*; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -41,9 +37,16 @@ import sonia.scm.repository.spi.HookChangesetProvider; import sonia.scm.repository.spi.HookChangesetRequest; import sonia.scm.repository.spi.HookChangesetResponse; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + /** * Unit tests for {@link HgHookTagProvider}. - * + * * @author Sebastian Sdorra */ @RunWith(MockitoJUnitRunner.class) @@ -51,7 +54,7 @@ public class HgHookTagProviderTest { @Mock private HookChangesetProvider changesetProvider; - + @InjectMocks private HgHookTagProvider tagProvider; @@ -61,9 +64,9 @@ public class HgHookTagProviderTest { @Test public void testGetDeletedTags() { prepareChangesets(new Changeset("1", Long.MIN_VALUE, null)); - assertThat(tagProvider.getDeletedTags(), empty()); + assertThat(tagProvider.getDeletedTags()).isEmpty(); } - + /** * Tests {@link HgHookTagProvider#getCreatedTags()}. */ @@ -71,22 +74,24 @@ public class HgHookTagProviderTest { public void testGetCreatedTags(){ Changeset c1 = new Changeset("1", Long.MIN_VALUE, null); c1.getTags().add("1.0.0"); - Changeset c2 = new Changeset("2", Long.MIN_VALUE, null); + Changeset c2 = new Changeset("2", Long.MAX_VALUE, null); c2.getTags().add("2.0.0"); Changeset c3 = new Changeset("3", Long.MIN_VALUE, null); prepareChangesets(c1, c2, c3); - + List tags = tagProvider.getCreatedTags(); assertNotNull(tags); assertEquals(2, tags.size()); - + Tag t1 = tags.get(0); assertEquals("1", t1.getRevision()); assertEquals("1.0.0", t1.getName()); - + assertThat(t1.getDate()).contains(Long.MIN_VALUE); + Tag t2 = tags.get(1); assertEquals("2", t2.getRevision()); assertEquals("2.0.0", t2.getName()); + assertThat(t2.getDate()).contains(Long.MAX_VALUE); } private void prepareChangesets(Changeset... changesets){ 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..e9bb65bf89 --- /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()).contains(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..7d4fd6dada 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,9 +21,10 @@ * 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.annotation.JsonInclude; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; @@ -31,6 +32,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.time.Instant; + @Getter @Setter @NoArgsConstructor @@ -40,6 +43,9 @@ public class TagDto extends HalRepresentation { private String revision; + @JsonInclude(JsonInclude.Include.NON_NULL) + 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..940b33dd05 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,9 @@ import sonia.scm.web.EdisonHalAppender; import javax.inject.Inject; +import java.time.Instant; +import java.util.Optional; + 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 +50,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 +66,9 @@ public abstract class TagToTagDtoMapper extends HalAppenderMapper { return new TagDto(linksBuilder.build(), embeddedBuilder.build()); } + + @Named("mapDate") + Instant map(Optional value) { + return value.map(Instant::ofEpochMilli).orElse(null); + } } 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)); + } + }