mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 07:25:44 +01:00
Display all tags for changeset
Display of all tags (as links to the overview of the specific tag) of a given changeset in the changeset detail view.
This commit is contained in:
2
gradle/changelog/tags_for_revision.yaml
Normal file
2
gradle/changelog/tags_for_revision.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Display of all tags for a given changeset in the changeset detail view
|
||||||
@@ -53,5 +53,11 @@ public enum Feature
|
|||||||
*
|
*
|
||||||
* @since 2.47.0
|
* @since 2.47.0
|
||||||
*/
|
*/
|
||||||
FORCE_PUSH
|
FORCE_PUSH,
|
||||||
|
/**
|
||||||
|
* The repository supports computation of tags for a given revision.
|
||||||
|
*
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
TAGS_FOR_REVISION
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.cache.Cache;
|
import sonia.scm.cache.Cache;
|
||||||
import sonia.scm.cache.CacheManager;
|
import sonia.scm.cache.CacheManager;
|
||||||
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryCacheKey;
|
import sonia.scm.repository.RepositoryCacheKey;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
@@ -50,10 +51,10 @@ import java.util.List;
|
|||||||
* TagsCommandBuilder tagsCommand = repositoryService.getTagsCommand();
|
* TagsCommandBuilder tagsCommand = repositoryService.getTagsCommand();
|
||||||
* Tags tags = tagsCommand.getTags();
|
* Tags tags = tagsCommand.getTags();
|
||||||
* </code></pre>
|
* </code></pre>
|
||||||
|
*
|
||||||
* @since 1.18
|
* @since 1.18
|
||||||
*/
|
*/
|
||||||
public final class TagsCommandBuilder
|
public final class TagsCommandBuilder {
|
||||||
{
|
|
||||||
|
|
||||||
static final String CACHE_NAME = "sonia.cache.cmd.tags";
|
static final String CACHE_NAME = "sonia.cache.cmd.tags";
|
||||||
|
|
||||||
@@ -61,7 +62,9 @@ public final class TagsCommandBuilder
|
|||||||
private static final Logger logger =
|
private static final Logger logger =
|
||||||
LoggerFactory.getLogger(TagsCommandBuilder.class);
|
LoggerFactory.getLogger(TagsCommandBuilder.class);
|
||||||
|
|
||||||
/** cache for changesets */
|
/**
|
||||||
|
* cache for changesets
|
||||||
|
*/
|
||||||
private final Cache<CacheKey, Tags> cache;
|
private final Cache<CacheKey, Tags> cache;
|
||||||
|
|
||||||
private final TagsCommand command;
|
private final TagsCommand command;
|
||||||
@@ -70,6 +73,8 @@ public final class TagsCommandBuilder
|
|||||||
|
|
||||||
private boolean disableCache = false;
|
private boolean disableCache = false;
|
||||||
|
|
||||||
|
private String revision;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new {@link TagsCommandBuilder}, this constructor should
|
* Constructs a new {@link TagsCommandBuilder}, this constructor should
|
||||||
* only be called from the {@link RepositoryService}.
|
* only be called from the {@link RepositoryService}.
|
||||||
@@ -79,8 +84,7 @@ public final class TagsCommandBuilder
|
|||||||
* @param repository repository
|
* @param repository repository
|
||||||
*/
|
*/
|
||||||
TagsCommandBuilder(CacheManager cacheManager, TagsCommand command,
|
TagsCommandBuilder(CacheManager cacheManager, TagsCommand command,
|
||||||
Repository repository)
|
Repository repository) {
|
||||||
{
|
|
||||||
this.cache = cacheManager.getCache(CACHE_NAME);
|
this.cache = cacheManager.getCache(CACHE_NAME);
|
||||||
this.command = command;
|
this.command = command;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
@@ -91,97 +95,78 @@ public final class TagsCommandBuilder
|
|||||||
* Returns all tags from the repository.
|
* Returns all tags from the repository.
|
||||||
*/
|
*/
|
||||||
public Tags getTags() throws IOException {
|
public Tags getTags() throws IOException {
|
||||||
Tags tags;
|
if (revision != null) {
|
||||||
|
logger.debug("get tags for repository {} with revision {}", repository, revision);
|
||||||
if (disableCache)
|
return getTagsFromCommandForRevision();
|
||||||
{
|
} else if (disableCache) {
|
||||||
if (logger.isDebugEnabled())
|
logger.debug("get tags for repository {} with disabled cache", repository);
|
||||||
{
|
return getTagsFromCommand();
|
||||||
logger.debug("get tags for repository {} with disabled cache",
|
} else {
|
||||||
repository.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
tags = getTagsFromCommand();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CacheKey key = new CacheKey(repository);
|
CacheKey key = new CacheKey(repository);
|
||||||
|
Tags tags = cache.get(key);
|
||||||
tags = cache.get(key);
|
if (tags == null) {
|
||||||
|
|
||||||
if (tags == null)
|
|
||||||
{
|
|
||||||
if (logger.isDebugEnabled())
|
|
||||||
{
|
|
||||||
logger.debug("get tags for repository {}", repository);
|
logger.debug("get tags for repository {}", repository);
|
||||||
}
|
|
||||||
|
|
||||||
tags = getTagsFromCommand();
|
tags = getTagsFromCommand();
|
||||||
|
|
||||||
if (tags != null)
|
|
||||||
{
|
|
||||||
cache.put(key, tags);
|
cache.put(key, tags);
|
||||||
|
} else {
|
||||||
|
logger.debug("get tags for repository {} from cache", repository);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (logger.isDebugEnabled())
|
|
||||||
{
|
|
||||||
logger.debug("get tags for repository {} from cache",
|
|
||||||
repository.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables the cache for tags. This means that every {@link Tag}
|
* Disables the cache for tags. This means that every {@link Tag}
|
||||||
* is directly retrieved from the {@link Repository}. <b>Note: </b> Disabling
|
* is directly retrieved from the {@link Repository}. <b>Note: </b> Disabling
|
||||||
* the cache cost a lot of performance and could be much slower.
|
* the cache cost a lot of performance and could be much slower.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param disableCache true to disable the cache
|
* @param disableCache true to disable the cache
|
||||||
*
|
|
||||||
* @return {@code this}
|
* @return {@code this}
|
||||||
*/
|
*/
|
||||||
public TagsCommandBuilder setDisableCache(boolean disableCache)
|
public TagsCommandBuilder setDisableCache(boolean disableCache) {
|
||||||
{
|
|
||||||
this.disableCache = disableCache;
|
this.disableCache = disableCache;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set revision to show all tags containing the given revision. This is only supported for repositories supporting
|
||||||
|
* feature {@link sonia.scm.repository.Feature#TAGS_FOR_REVISION} (@see {@link RepositoryService#isSupported(Feature)}).
|
||||||
|
*
|
||||||
|
* @return {@code this}
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public TagsCommandBuilder forRevision(String revision) {
|
||||||
|
this.revision = revision;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
private Tags getTagsFromCommand() throws IOException
|
private Tags getTagsFromCommand() throws IOException {
|
||||||
{
|
|
||||||
List<Tag> tagList = command.getTags();
|
List<Tag> tagList = command.getTags();
|
||||||
|
|
||||||
return new Tags(tagList);
|
return new Tags(tagList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Tags getTagsFromCommandForRevision() throws IOException {
|
||||||
|
return new Tags(command.getTags(revision));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class CacheKey implements RepositoryCacheKey {
|
||||||
static class CacheKey implements RepositoryCacheKey
|
|
||||||
{
|
|
||||||
private final String repositoryId;
|
private final String repositoryId;
|
||||||
|
|
||||||
public CacheKey(Repository repository)
|
public CacheKey(Repository repository) {
|
||||||
{
|
|
||||||
this.repositoryId = repository.getId();
|
this.repositoryId = repository.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj)
|
public boolean equals(Object obj) {
|
||||||
{
|
if (obj == null) {
|
||||||
if (obj == null)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getClass() != obj.getClass())
|
if (getClass() != obj.getClass()) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,15 +177,13 @@ public final class TagsCommandBuilder
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode()
|
public int hashCode() {
|
||||||
{
|
|
||||||
return Objects.hashCode(repositoryId);
|
return Objects.hashCode(repositoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getRepositoryId()
|
public String getRepositoryId() {
|
||||||
{
|
|
||||||
return repositoryId;
|
return repositoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,15 +25,20 @@
|
|||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
|
||||||
|
import sonia.scm.FeatureNotSupportedException;
|
||||||
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
public interface TagsCommand
|
public interface TagsCommand {
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
public List<Tag> getTags() throws IOException;
|
public List<Tag> getTags() throws IOException;
|
||||||
|
|
||||||
|
default List<Tag> getTags(String revision) throws IOException{
|
||||||
|
throw new FeatureNotSupportedException(Feature.TAGS_FOR_REVISION.name());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
|
|||||||
protected static final Set<Feature> FEATURES = EnumSet.of(
|
protected static final Set<Feature> FEATURES = EnumSet.of(
|
||||||
Feature.INCOMING_REVISION,
|
Feature.INCOMING_REVISION,
|
||||||
Feature.MODIFICATIONS_BETWEEN_REVISIONS,
|
Feature.MODIFICATIONS_BETWEEN_REVISIONS,
|
||||||
Feature.FORCE_PUSH
|
Feature.FORCE_PUSH,
|
||||||
|
Feature.TAGS_FOR_REVISION
|
||||||
);
|
);
|
||||||
|
|
||||||
private final Injector injector;
|
private final Injector injector;
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ import com.google.inject.assistedinject.Assisted;
|
|||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.lib.Ref;
|
import org.eclipse.jgit.lib.Ref;
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import sonia.scm.repository.GitUtil;
|
||||||
import sonia.scm.repository.InternalRepositoryException;
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
|
|
||||||
@@ -54,8 +56,19 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Tag> getTags() throws IOException {
|
public List<Tag> getTags() throws IOException {
|
||||||
|
return getTags(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Tag> getTags(String revision) throws IOException {
|
||||||
try (Git git = new Git(open()); RevWalk revWalk = new RevWalk(git.getRepository())) {
|
try (Git git = new Git(open()); RevWalk revWalk = new RevWalk(git.getRepository())) {
|
||||||
List<Ref> tagList = git.tagList().call();
|
List<Ref> tagList;
|
||||||
|
|
||||||
|
if (revision != null) {
|
||||||
|
tagList = git.tagList().setContains(GitUtil.getRevisionId(git.getRepository(), revision)).call();
|
||||||
|
} else {
|
||||||
|
tagList = git.tagList().call();
|
||||||
|
}
|
||||||
|
|
||||||
return tagList.stream()
|
return tagList.stream()
|
||||||
.map(ref -> gitTagConverter.buildTag(git.getRepository(), revWalk, ref))
|
.map(ref -> gitTagConverter.buildTag(git.getRepository(), revWalk, ref))
|
||||||
|
|||||||
@@ -70,27 +70,51 @@ public class GitTagsCommandTest extends AbstractGitCommandTestBase {
|
|||||||
assertThat(lightweightTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
assertThat(lightweightTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetTagsForRevision() throws IOException {
|
||||||
|
GitContext gitContext = createContext();
|
||||||
|
GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, new GitTagConverter(gpg));
|
||||||
|
|
||||||
|
List<Tag> tags = tagsCommand.getTags("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
||||||
|
|
||||||
|
assertThat(tags).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetTagsForShortenedRevision() throws IOException {
|
||||||
|
GitContext gitContext = createContext();
|
||||||
|
GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, new GitTagConverter(gpg));
|
||||||
|
|
||||||
|
List<Tag> tags = tagsCommand.getTags("86a6645");
|
||||||
|
|
||||||
|
assertThat(tags).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetSignatures() throws IOException {
|
public void shouldGetSignatures() throws IOException {
|
||||||
when(gpg.findPublicKeyId(ArgumentMatchers.any())).thenReturn("2BA27721F113C005CC16F06BAE63EFBC49F140CF");
|
when(gpg.findPublicKeyId(ArgumentMatchers.any())).thenReturn("2BA27721F113C005CC16F06BAE63EFBC49F140CF");
|
||||||
when(gpg.findPublicKey("2BA27721F113C005CC16F06BAE63EFBC49F140CF")).thenReturn(Optional.of(publicKey));
|
when(gpg.findPublicKey("2BA27721F113C005CC16F06BAE63EFBC49F140CF")).thenReturn(Optional.of(publicKey));
|
||||||
String signature = "-----BEGIN PGP SIGNATURE-----\n" +
|
String signature = """
|
||||||
"\n" +
|
-----BEGIN PGP SIGNATURE-----
|
||||||
"iQEzBAABCgAdFiEEK6J3IfETwAXMFvBrrmPvvEnxQM8FAl+9acoACgkQrmPvvEnx\n" +
|
|
||||||
"QM9abwgAnGP+Y/Ijli+PAsimfOmZQWYepjptoOv9m7i3bnHv8V+Qg6cm51I3E0YV\n" +
|
iQEzBAABCgAdFiEEK6J3IfETwAXMFvBrrmPvvEnxQM8FAl+9acoACgkQrmPvvEnx
|
||||||
"R2QaxxzW9PgS4hcES+L1qs8Lwo18RurF469eZEmNb8DcUFJ3sEWeHlIl5wZNNo/v\n" +
|
QM9abwgAnGP+Y/Ijli+PAsimfOmZQWYepjptoOv9m7i3bnHv8V+Qg6cm51I3E0YV
|
||||||
"jJm0d9LNcSmtAIiQ8eDMoGdFXJzHewGickLOSsQGmfZgZus4Qlsh7r3BZTI1Zwd/\n" +
|
R2QaxxzW9PgS4hcES+L1qs8Lwo18RurF469eZEmNb8DcUFJ3sEWeHlIl5wZNNo/v
|
||||||
"6jaBFctX13FuepCTxq2SjEfRaQHIYkyFQq2o6mjL5S2qfYJ/S//gcCCzxllQrisF\n" +
|
jJm0d9LNcSmtAIiQ8eDMoGdFXJzHewGickLOSsQGmfZgZus4Qlsh7r3BZTI1Zwd/
|
||||||
"5fRW3LzLI4eXFH0vua7+UzNS2Rwpifg2OENJA/Kn+3R36LWEGxFK9pNqjVPRAcQj\n" +
|
6jaBFctX13FuepCTxq2SjEfRaQHIYkyFQq2o6mjL5S2qfYJ/S//gcCCzxllQrisF
|
||||||
"1vSkcjK26RqhAqCjNLSagM8ATZrh+g==\n" +
|
5fRW3LzLI4eXFH0vua7+UzNS2Rwpifg2OENJA/Kn+3R36LWEGxFK9pNqjVPRAcQj
|
||||||
"=kUKm\n" +
|
1vSkcjK26RqhAqCjNLSagM8ATZrh+g==
|
||||||
"-----END PGP SIGNATURE-----\n";
|
=kUKm
|
||||||
String signedContent = "object 592d797cd36432e591416e8b2b98154f4f163411\n" +
|
-----END PGP SIGNATURE-----
|
||||||
"type commit\n" +
|
""";
|
||||||
"tag signedtag\n" +
|
String signedContent = """
|
||||||
"tagger Arthur Dent <arthur.dent@hitchhiker.com> 1606248906 +0100\n" +
|
object 592d797cd36432e591416e8b2b98154f4f163411
|
||||||
"\n" +
|
type commit
|
||||||
"this tag is signed\n";
|
tag signedtag
|
||||||
|
tagger Arthur Dent <arthur.dent@hitchhiker.com> 1606248906 +0100
|
||||||
|
|
||||||
|
this tag is signed
|
||||||
|
""";
|
||||||
when(publicKey.verify(signedContent.getBytes(), signature.getBytes())).thenReturn(true);
|
when(publicKey.verify(signedContent.getBytes(), signature.getBytes())).thenReturn(true);
|
||||||
|
|
||||||
final GitContext gitContext = createContext();
|
final GitContext gitContext = createContext();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { Changeset, Link, NamespaceAndName, Repository, Tag, TagCollection } from "@scm-manager/ui-types";
|
import { Changeset, Link, NamespaceAndName, Repository, Tag, TagCollection } from "@scm-manager/ui-types";
|
||||||
import { requiredLink } from "./links";
|
import { objectLink, requiredLink } from "./links";
|
||||||
import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
|
import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
import { ApiResult } from "./base";
|
import { ApiResult } from "./base";
|
||||||
import { repoQueryKey } from "./keys";
|
import { repoQueryKey } from "./keys";
|
||||||
@@ -44,6 +44,23 @@ export const useTags = (repository: Repository): ApiResult<TagCollection> => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useContainedInTags = (changeset: Changeset, repository: Repository): ApiResult<TagCollection> => {
|
||||||
|
const link = objectLink(changeset, "containedInTags");
|
||||||
|
|
||||||
|
return useQuery<TagCollection, Error>(repoQueryKey(repository, "tags", changeset.id), () => {
|
||||||
|
if (link === null) {
|
||||||
|
return {
|
||||||
|
_embedded: {
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
_links: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.get(link).then((response) => response.json());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useTag = (repository: Repository, name: string): ApiResult<Tag> => {
|
export const useTag = (repository: Repository, name: string): ApiResult<Tag> => {
|
||||||
const link = requiredLink(repository, "tags");
|
const link = requiredLink(repository, "tags");
|
||||||
return useQuery<Tag, Error>(tagQueryKey(repository, name), () =>
|
return useQuery<Tag, Error>(tagQueryKey(repository, name), () =>
|
||||||
@@ -62,10 +79,14 @@ const invalidateCacheForTag = (queryClient: QueryClient, repository: NamespaceAn
|
|||||||
const createTag = (changeset: Changeset, link: string) => {
|
const createTag = (changeset: Changeset, link: string) => {
|
||||||
return (name: string) => {
|
return (name: string) => {
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(link, {
|
.post(
|
||||||
|
link,
|
||||||
|
{
|
||||||
name,
|
name,
|
||||||
revision: changeset.id,
|
revision: changeset.id,
|
||||||
})
|
},
|
||||||
|
"application/vnd.scmm-tagRequest+json;v=2"
|
||||||
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const location = response.headers.get("Location");
|
const location = response.headers.get("Location");
|
||||||
if (!location) {
|
if (!location) {
|
||||||
|
|||||||
@@ -307,6 +307,13 @@
|
|||||||
},
|
},
|
||||||
"tag": {
|
"tag": {
|
||||||
"create": "Tag erstellen"
|
"create": "Tag erstellen"
|
||||||
|
},
|
||||||
|
"containedInTags": {
|
||||||
|
"containedInTag_one": "Enthalten in {{count}} Tag",
|
||||||
|
"containedInTag_other": "Enthalten in {{count}} Tags",
|
||||||
|
"allTags": "Alle Tags, in denen der Commit enthalten ist",
|
||||||
|
"showAllTags": "Alle Tags, in denen der Commit enthalten ist, einblenden",
|
||||||
|
"hideAllTags": "Alle Tags, in denen der Commit enthalten ist, ausblenden"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commit": {
|
"commit": {
|
||||||
|
|||||||
@@ -307,6 +307,13 @@
|
|||||||
},
|
},
|
||||||
"tag": {
|
"tag": {
|
||||||
"create": "Create Tag"
|
"create": "Create Tag"
|
||||||
|
},
|
||||||
|
"containedInTags": {
|
||||||
|
"containedInTag_one": "Contained in {{count}} tag",
|
||||||
|
"containedInTag_other": "Contained in {{count}} tags",
|
||||||
|
"allTags": "All tags the commit is contained in",
|
||||||
|
"showAllTags": "Show all tags the commit is contained in",
|
||||||
|
"hideAllTags": "Hide all tags the commit is contained in"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commit": {
|
"commit": {
|
||||||
|
|||||||
@@ -42,12 +42,13 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
Level,
|
Level,
|
||||||
SignatureIcon,
|
SignatureIcon,
|
||||||
Tooltip,
|
|
||||||
SubSubtitle,
|
SubSubtitle,
|
||||||
|
Tooltip,
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import ContributorTable from "./ContributorTable";
|
import ContributorTable from "./ContributorTable";
|
||||||
import { Link as ReactLink } from "react-router-dom";
|
import { Link, Link as ReactLink } from "react-router-dom";
|
||||||
import CreateTagModal from "./CreateTagModal";
|
import CreateTagModal from "./CreateTagModal";
|
||||||
|
import { useContainedInTags } from "@scm-manager/ui-api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
changeset: Changeset;
|
changeset: Changeset;
|
||||||
@@ -74,7 +75,7 @@ const countContributors = (changeset: Changeset) => {
|
|||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContributorColumn = styled.p`
|
const ContributorColumn = styled.div`
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -82,7 +83,7 @@ const ContributorColumn = styled.p`
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CountColumn = styled.p`
|
const CountColumn = styled.div`
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
@@ -97,8 +98,11 @@ const SeparatedParents = styled.div`
|
|||||||
const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
|
const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const signatureIcon = changeset?.signatures && changeset.signatures.length > 0 && (
|
const signatureIcon =
|
||||||
|
changeset?.signatures && changeset.signatures.length > 0 ? (
|
||||||
<SignatureIcon className="mx-2" signatures={changeset.signatures} />
|
<SignatureIcon className="mx-2" signatures={changeset.signatures} />
|
||||||
|
) : (
|
||||||
|
<> </>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -116,22 +120,62 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className="is-flex is-clickable" onClick={(e) => setOpen(!open)}>
|
<div className="is-flex is-clickable" onClick={(e) => setOpen(!open)}>
|
||||||
<ContributorColumn className="is-ellipsis-overflow">
|
<ContributorColumn className="is-ellipsis-overflow">
|
||||||
<Icon name="angle-right" alt={t("changeset.contributors.showList")} />{" "}
|
<Icon name="angle-right" alt={t("changeset.contributors.showList")} /> <ChangesetAuthor changeset={changeset} />
|
||||||
<ChangesetAuthor changeset={changeset} />
|
|
||||||
</ContributorColumn>
|
</ContributorColumn>
|
||||||
{signatureIcon}
|
{signatureIcon}
|
||||||
<CountColumn className="is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only">
|
<CountColumn className="is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only">
|
||||||
(
|
(<span>{t("changeset.contributors.count", { count: countContributors(changeset) })}</span>)
|
||||||
<span className="has-text-link">
|
|
||||||
{t("changeset.contributors.count", { count: countContributors(changeset) })}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
</CountColumn>
|
</CountColumn>
|
||||||
</div>
|
</div>
|
||||||
</>
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContainedInTags: FC<{ changeset: Changeset; repository: Repository }> = ({ changeset, repository }) => {
|
||||||
|
const [t] = useTranslation("repos");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { data, isLoading } = useContainedInTags(changeset, repository);
|
||||||
|
|
||||||
|
const tags = data?._embedded?.tags;
|
||||||
|
|
||||||
|
if (!tags || tags.length === 0 || isLoading) {
|
||||||
|
return <div className="mb-5"></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
return (
|
||||||
|
<div className="is-flex is-flex-direction-column mb-4">
|
||||||
|
<div className="is-flex">
|
||||||
|
<p className="is-ellipsis-overflow is-clickable mb-2" onClick={(e) => setOpen(!open)}>
|
||||||
|
<Icon name="angle-down" alt={t("changeset.containedInTags.hideAllTags")} />{" "}
|
||||||
|
{t("changeset.containedInTags.allTags")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{" "}
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<span className="tag is-info is-normal m-1">
|
||||||
|
<Link
|
||||||
|
to={`/repo/${repository.namespace}/${repository.name}/tag/${tag.name}`}
|
||||||
|
className="has-text-inherit"
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="is-flex is-clickable" onClick={() => setOpen(!open)}>
|
||||||
|
<ContributorColumn className="is-ellipsis-overflow">
|
||||||
|
<Icon name="angle-right" alt={t("changeset.containedInTags.showAllTags")} />{" "}
|
||||||
|
{t("changeset.containedInTags.containedInTag", { count: tags.length })}
|
||||||
|
</ContributorColumn>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,6 +221,7 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
|
|||||||
</AvatarWrapper>
|
</AvatarWrapper>
|
||||||
<div className="media-content">
|
<div className="media-content">
|
||||||
<Contributors changeset={changeset} />
|
<Contributors changeset={changeset} />
|
||||||
|
<ContainedInTags changeset={changeset} repository={repository} />
|
||||||
<div className="is-flex is-ellipsis-overflow">
|
<div className="is-flex is-ellipsis-overflow">
|
||||||
<p>
|
<p>
|
||||||
<Trans i18nKey="repos:changeset.summary" components={[id, date]} />
|
<Trans i18nKey="repos:changeset.summary" components={[id, date]} />
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public class ChangesetRootResource {
|
|||||||
mediaType = VndMediaType.ERROR_TYPE,
|
mediaType = VndMediaType.ERROR_TYPE,
|
||||||
schema = @Schema(implementation = ErrorDto.class)
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
))
|
))
|
||||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException {
|
public ChangesetDto get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException {
|
||||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||||
Repository repository = repositoryService.getRepository();
|
Repository repository = repositoryService.getRepository();
|
||||||
RepositoryPermissions.read(repository).check();
|
RepositoryPermissions.read(repository).check();
|
||||||
@@ -156,7 +156,7 @@ public class ChangesetRootResource {
|
|||||||
if (changeset == null) {
|
if (changeset == null) {
|
||||||
throw notFound(entity(Changeset.class, id).in(repository));
|
throw notFound(entity(Changeset.class, id).in(repository));
|
||||||
}
|
}
|
||||||
return Response.ok(changesetToChangesetDtoMapper.map(changeset, repository)).build();
|
return changesetToChangesetDtoMapper.map(changeset, repository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import org.mapstruct.ObjectFactory;
|
|||||||
import sonia.scm.repository.Branch;
|
import sonia.scm.repository.Branch;
|
||||||
import sonia.scm.repository.Changeset;
|
import sonia.scm.repository.Changeset;
|
||||||
import sonia.scm.repository.Contributor;
|
import sonia.scm.repository.Contributor;
|
||||||
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.Person;
|
import sonia.scm.repository.Person;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryPermissions;
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
@@ -125,6 +126,9 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
|
|||||||
if (repositoryService.isSupported(Command.TAG) && RepositoryPermissions.push(repository).isPermitted()) {
|
if (repositoryService.isSupported(Command.TAG) && RepositoryPermissions.push(repository).isPermitted()) {
|
||||||
linksBuilder.single(link("tag", resourceLinks.tag().create(namespace, name)));
|
linksBuilder.single(link("tag", resourceLinks.tag().create(namespace, name)));
|
||||||
}
|
}
|
||||||
|
if (repositoryService.isSupported(Command.TAGS) && repositoryService.isSupported(Feature.TAGS_FOR_REVISION)) {
|
||||||
|
linksBuilder.single(link("containedInTags", resourceLinks.tag().getForChangeset(namespace, name, source.getId())));
|
||||||
|
}
|
||||||
if (repositoryService.isSupported(Command.BRANCHES)) {
|
if (repositoryService.isSupported(Command.BRANCHES)) {
|
||||||
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository,
|
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository,
|
||||||
getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId()))));
|
getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId()))));
|
||||||
|
|||||||
@@ -570,6 +570,10 @@ class ResourceLinks {
|
|||||||
String all(String namespace, String name) {
|
String all(String namespace, String name) {
|
||||||
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href();
|
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getForChangeset(String namespace, String name, String changeset) {
|
||||||
|
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getForChangeset").parameters(changeset).href();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DiffLinks diff() {
|
public DiffLinks diff() {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.google.inject.Inject;
|
|||||||
import de.otto.edison.hal.Embedded;
|
import de.otto.edison.hal.Embedded;
|
||||||
import de.otto.edison.hal.HalRepresentation;
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
import de.otto.edison.hal.Links;
|
import de.otto.edison.hal.Links;
|
||||||
|
import sonia.scm.repository.Changeset;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
|
|
||||||
@@ -53,6 +54,10 @@ public class TagCollectionToDtoMapper {
|
|||||||
return new HalRepresentation(createLinks(repository.getNamespace(), repository.getName()), embedDtos(getTagDtoList(tags, repository)));
|
return new HalRepresentation(createLinks(repository.getNamespace(), repository.getName()), embedDtos(getTagDtoList(tags, repository)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HalRepresentation map(Collection<Tag> tags, Repository repository, String changeset) {
|
||||||
|
return new HalRepresentation(createLinks(repository.getNamespace(), repository.getName(), changeset), embedDtos(getTagDtoList(tags, repository)));
|
||||||
|
}
|
||||||
|
|
||||||
public List<TagDto> getTagDtoList(Collection<Tag> tags, Repository repository) {
|
public List<TagDto> getTagDtoList(Collection<Tag> tags, Repository repository) {
|
||||||
return tags.stream().map(tag -> tagToTagDtoMapper.map(tag, repository)).collect(toList());
|
return tags.stream().map(tag -> tagToTagDtoMapper.map(tag, repository)).collect(toList());
|
||||||
}
|
}
|
||||||
@@ -75,6 +80,13 @@ public class TagCollectionToDtoMapper {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Links createLinks(String namespace, String name, String changeset) {
|
||||||
|
return
|
||||||
|
linkingTo()
|
||||||
|
.self(resourceLinks.tag().getForChangeset(namespace, name, changeset))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private Embedded embedDtos(List<TagDto> dtos) {
|
private Embedded embedDtos(List<TagDto> dtos) {
|
||||||
return embeddedBuilder()
|
return embeddedBuilder()
|
||||||
.with("tags", dtos)
|
.with("tags", dtos)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.headers.Header;
|
import io.swagger.v3.oas.annotations.headers.Header;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
@@ -33,6 +34,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.DELETE;
|
import jakarta.ws.rs.DELETE;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
@@ -49,6 +51,7 @@ import sonia.scm.repository.Tags;
|
|||||||
import sonia.scm.repository.api.RepositoryService;
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
import sonia.scm.repository.api.TagCommandBuilder;
|
import sonia.scm.repository.api.TagCommandBuilder;
|
||||||
|
import sonia.scm.repository.api.TagsCommandBuilder;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -90,6 +93,13 @@ public class TagRootResource {
|
|||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "not found, repository does not exist",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = VndMediaType.ERROR_TYPE,
|
||||||
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
|
))
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "500",
|
responseCode = "500",
|
||||||
description = "internal server error",
|
description = "internal server error",
|
||||||
@@ -112,7 +122,7 @@ public class TagRootResource {
|
|||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("")
|
@Path("")
|
||||||
@Produces(VndMediaType.TAG_REQUEST)
|
@Consumes(VndMediaType.TAG_REQUEST)
|
||||||
@Operation(summary = "Create tag",
|
@Operation(summary = "Create tag",
|
||||||
description = "Creates a new tag.",
|
description = "Creates a new tag.",
|
||||||
tags = "Repository",
|
tags = "Repository",
|
||||||
@@ -140,11 +150,12 @@ public class TagRootResource {
|
|||||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "not found, no tag with the specified name available in the repository",
|
description = "not found, repository does not exist",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
mediaType = VndMediaType.ERROR_TYPE,
|
mediaType = VndMediaType.ERROR_TYPE,
|
||||||
schema = @Schema(implementation = ErrorDto.class)
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
))
|
))
|
||||||
|
@ApiResponse(responseCode = "409", description = "conflict, tag with given id already exists in repository")
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "500",
|
responseCode = "500",
|
||||||
description = "internal server error",
|
description = "internal server error",
|
||||||
@@ -187,7 +198,7 @@ public class TagRootResource {
|
|||||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "not found, no tag with the specified name available in the repository",
|
description = "not found, no tag with the specified name available in the repository or the repository does not exist",
|
||||||
content = @Content(
|
content = @Content(
|
||||||
mediaType = VndMediaType.ERROR_TYPE,
|
mediaType = VndMediaType.ERROR_TYPE,
|
||||||
schema = @Schema(implementation = ErrorDto.class)
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
@@ -217,9 +228,43 @@ public class TagRootResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("contains/{changeset}")
|
||||||
|
@Produces(VndMediaType.TAG_COLLECTION)
|
||||||
|
@Operation(summary = "Get tags for specific revision", description = "Returns all tags related to a given revision", tags = "Repository")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "success",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = VndMediaType.TAG_COLLECTION,
|
||||||
|
schema = @Schema(implementation = CollectionDto.class)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||||
|
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "not found, repository does not exist",
|
||||||
|
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 HalRepresentation getForChangeset(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("changeset") String changeset) throws IOException {
|
||||||
|
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||||
|
TagsCommandBuilder tagsCommandBuilder = repositoryService.getTagsCommand();
|
||||||
|
return tagCollectionToDtoMapper.map(tagsCommandBuilder.forRevision(changeset).getTags().getTags(), repositoryService.getRepository(), changeset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("{tagName}")
|
@Path("{tagName}")
|
||||||
@Produces(VndMediaType.TAG)
|
|
||||||
@Operation(summary = "Delete tag", description = "Deletes the tag provided in the path", tags = "Repository")
|
@Operation(summary = "Delete tag", description = "Deletes the tag provided in the path", tags = "Repository")
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
|
||||||
import com.google.inject.util.Providers;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||||
@@ -34,68 +33,80 @@ import org.apache.shiro.util.ThreadState;
|
|||||||
import org.assertj.core.util.Lists;
|
import org.assertj.core.util.Lists;
|
||||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
import org.junit.After;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.Before;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.Test;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.repository.Changeset;
|
import sonia.scm.repository.Changeset;
|
||||||
import sonia.scm.repository.ChangesetPagingResult;
|
import sonia.scm.repository.ChangesetPagingResult;
|
||||||
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
import sonia.scm.repository.Person;
|
import sonia.scm.repository.Person;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.api.Command;
|
||||||
import sonia.scm.repository.api.LogCommandBuilder;
|
import sonia.scm.repository.api.LogCommandBuilder;
|
||||||
import sonia.scm.repository.api.RepositoryService;
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
|
import sonia.scm.web.JsonMockHttpResponse;
|
||||||
import sonia.scm.web.RestDispatcher;
|
import sonia.scm.web.RestDispatcher;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mock.Strictness.LENIENT;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ChangesetRootResourceTest extends RepositoryTestBase {
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ChangesetRootResourceTest extends RepositoryTestBase {
|
||||||
|
|
||||||
public static final String CHANGESET_PATH = "space/repo/changesets/";
|
static final String CHANGESET_PATH = "space/repo/changesets/";
|
||||||
public static final String CHANGESET_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + CHANGESET_PATH;
|
static final String CHANGESET_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + CHANGESET_PATH;
|
||||||
|
|
||||||
private RestDispatcher dispatcher = new RestDispatcher();
|
private final RestDispatcher dispatcher = new RestDispatcher();
|
||||||
|
|
||||||
private final URI baseUri = URI.create("/");
|
private final URI baseUri = URI.create("/");
|
||||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||||
|
|
||||||
@Mock
|
@Mock(strictness = LENIENT)
|
||||||
private RepositoryServiceFactory serviceFactory;
|
private RepositoryServiceFactory serviceFactory;
|
||||||
|
|
||||||
@Mock
|
@Mock(strictness = LENIENT)
|
||||||
private RepositoryService repositoryService;
|
private RepositoryService repositoryService;
|
||||||
|
|
||||||
@Mock
|
@Mock(strictness = LENIENT)
|
||||||
private LogCommandBuilder logCommandBuilder;
|
private LogCommandBuilder logCommandBuilder;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TagCollectionToDtoMapper tagCollectionToDtoMapper;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
|
private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
|
||||||
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper;
|
private DefaultChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper;
|
||||||
|
|
||||||
private final Subject subject = mock(Subject.class);
|
private final Subject subject = mock(Subject.class);
|
||||||
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||||
|
|
||||||
@Before
|
@BeforeEach
|
||||||
public void prepareEnvironment() {
|
void prepareEnvironment() {
|
||||||
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
||||||
changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper);
|
changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper);
|
||||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||||
@@ -108,13 +119,13 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
|
|||||||
when(subject.isPermitted(any(String.class))).thenReturn(true);
|
when(subject.isPermitted(any(String.class))).thenReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@AfterEach
|
||||||
public void cleanupContext() {
|
void cleanupContext() {
|
||||||
ThreadContext.unbindSubject();
|
ThreadContext.unbindSubject();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetChangeSets() throws Exception {
|
void shouldGetChangeSets() throws Exception {
|
||||||
String id = "revision_123";
|
String id = "revision_123";
|
||||||
Instant creationDate = Instant.now();
|
Instant creationDate = Instant.now();
|
||||||
String authorName = "name";
|
String authorName = "name";
|
||||||
@@ -142,7 +153,7 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetSinglePageOfChangeSets() throws Exception {
|
void shouldGetSinglePageOfChangeSets() throws Exception {
|
||||||
String id = "revision_123";
|
String id = "revision_123";
|
||||||
Instant creationDate = Instant.now();
|
Instant creationDate = Instant.now();
|
||||||
String authorName = "name";
|
String authorName = "name";
|
||||||
@@ -169,33 +180,67 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
|
|||||||
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
|
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
public void shouldGetChangeSet() throws Exception {
|
class ForExistingChangeset {
|
||||||
String id = "revision_123";
|
|
||||||
Instant creationDate = Instant.now();
|
|
||||||
String authorName = "name";
|
|
||||||
String authorEmail = "em@i.l";
|
|
||||||
String commit = "my branch commit";
|
|
||||||
|
|
||||||
|
private final String id = "revision_123";
|
||||||
|
private final Instant creationDate = Instant.now();
|
||||||
|
private final String authorName = "name";
|
||||||
|
private final String authorEmail = "em@i.l";
|
||||||
|
private final String commit = "my branch commit";
|
||||||
|
|
||||||
|
private final JsonMockHttpResponse response = new JsonMockHttpResponse();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void prepareExistingChangeset() throws URISyntaxException, IOException {
|
||||||
when(logCommandBuilder.getChangeset(id)).thenReturn(
|
when(logCommandBuilder.getChangeset(id)).thenReturn(
|
||||||
new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)
|
new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeRequest() throws URISyntaxException {
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.get(CHANGESET_URL + id)
|
.get(CHANGESET_URL + id)
|
||||||
.accept(VndMediaType.CHANGESET);
|
.accept(VndMediaType.CHANGESET);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
assertEquals(200, response.getStatus());
|
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id)));
|
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName)));
|
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail)));
|
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldReturnNotFoundForNonExistingChangeset() throws Exception {
|
void shouldGetChangeSet() throws URISyntaxException {
|
||||||
|
executeRequest();
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
assertThat(response.getContentAsJson().get("id").asText()).isEqualTo(id);
|
||||||
|
assertThat(response.getContentAsJson().get("author").get("name").asText()).isEqualTo(authorName);
|
||||||
|
assertThat(response.getContentAsJson().get("author").get("mail").asText()).isEqualTo(authorEmail);
|
||||||
|
assertThat(response.getContentAsJson().get("description").asText()).isEqualTo(commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldContainLinkForTagCreation() throws URISyntaxException {
|
||||||
|
when(subject.isPermitted("repository:push:repoId")).thenReturn(true);
|
||||||
|
when(repositoryService.isSupported(Command.TAG)).thenReturn(true);
|
||||||
|
|
||||||
|
executeRequest();
|
||||||
|
|
||||||
|
assertThat(response.getContentAsJson().get("_links").get("tag").get("href").asText())
|
||||||
|
.isEqualTo("/v2/repositories/space/repo/tags/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldContainLinkForTagsForRevision() throws URISyntaxException {
|
||||||
|
when(repositoryService.isSupported(Command.TAGS)).thenReturn(true);
|
||||||
|
when(repositoryService.isSupported(Feature.TAGS_FOR_REVISION)).thenReturn(true);
|
||||||
|
|
||||||
|
executeRequest();
|
||||||
|
|
||||||
|
assertThat(response.getContentAsJson().get("_links").get("containedInTags").get("href").asText())
|
||||||
|
.isEqualTo("/v2/repositories/space/repo/tags/contains/revision_123");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnNotFoundForNonExistingChangeset() throws Exception {
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.get(CHANGESET_URL + "abcd")
|
.get(CHANGESET_URL + "abcd")
|
||||||
.accept(VndMediaType.CHANGESET);
|
.accept(VndMediaType.CHANGESET);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.shiro.authz.AuthorizationException;
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||||
@@ -37,6 +36,7 @@ import org.junit.After;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Answers;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
@@ -48,6 +48,7 @@ import sonia.scm.repository.api.RepositoryService;
|
|||||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
import sonia.scm.repository.api.TagCommandBuilder;
|
import sonia.scm.repository.api.TagCommandBuilder;
|
||||||
import sonia.scm.repository.api.TagsCommandBuilder;
|
import sonia.scm.repository.api.TagsCommandBuilder;
|
||||||
|
import sonia.scm.web.JsonMockHttpResponse;
|
||||||
import sonia.scm.web.RestDispatcher;
|
import sonia.scm.web.RestDispatcher;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ import java.net.URI;
|
|||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
@@ -66,7 +67,6 @@ import static org.mockito.Mockito.never;
|
|||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||||
public class TagRootResourceTest extends RepositoryTestBase {
|
public class TagRootResourceTest extends RepositoryTestBase {
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
|||||||
@Mock
|
@Mock
|
||||||
private RepositoryService repositoryService;
|
private RepositoryService repositoryService;
|
||||||
|
|
||||||
@Mock
|
@Mock(answer = Answers.RETURNS_SELF)
|
||||||
private TagsCommandBuilder tagsCommandBuilder;
|
private TagsCommandBuilder tagsCommandBuilder;
|
||||||
@Mock
|
@Mock
|
||||||
private TagCommandBuilder tagCommandBuilder;
|
private TagCommandBuilder tagCommandBuilder;
|
||||||
@@ -175,6 +175,23 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
|||||||
assertEquals(500, response.getStatus());
|
assertEquals(500, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetTagsForRevision() throws Exception {
|
||||||
|
final JsonMockHttpResponse response = new JsonMockHttpResponse();
|
||||||
|
Tags tags = new Tags();
|
||||||
|
String tag1 = "v1.0";
|
||||||
|
String revision1 = "revision_1234";
|
||||||
|
tags.setTags(Lists.newArrayList(new Tag(tag1, revision1)));
|
||||||
|
when(tagsCommandBuilder.forRevision(revision1).getTags()).thenReturn(tags);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.get(TAG_URL+ "contains/" + revision1)
|
||||||
|
.accept(VndMediaType.TAG_COLLECTION);
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getContentAsJson().get("_embedded").get("tags").get(0).get("name").asText()).isEqualTo(tag1);
|
||||||
|
assertThat(response.getContentAsJson().get("_links").get("self").get("href").asText()).isEqualTo("/v2/repositories/space/repo/tags/contains/revision_1234");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetTags() throws Exception {
|
public void shouldGetTags() throws Exception {
|
||||||
@@ -191,8 +208,8 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
|||||||
.accept(VndMediaType.TAG_COLLECTION);
|
.accept(VndMediaType.TAG_COLLECTION);
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
assertEquals(200, response.getStatus());
|
assertEquals(200, response.getStatus());
|
||||||
log.info("the content: ", response.getContentAsString());
|
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag1)));
|
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag1)));
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision1)));
|
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision1)));
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2)));
|
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2)));
|
||||||
@@ -224,7 +241,6 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
|||||||
response = new MockHttpResponse();
|
response = new MockHttpResponse();
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
assertEquals(200, response.getStatus());
|
assertEquals(200, response.getStatus());
|
||||||
log.info("the content: ", response.getContentAsString());
|
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2)));
|
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2)));
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision2)));
|
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision2)));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user