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:
Laura Gorzitze
2024-03-11 17:09:59 +01:00
parent 28a2de3cb3
commit 8d12862ff8
18 changed files with 411 additions and 171 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Display of all tags for a given changeset in the changeset detail view

View File

@@ -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
} }

View File

@@ -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,18 +51,20 @@ 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";
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,17 +73,18 @@ 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}.
* *
* @param cacheManager cache manager * @param cacheManager cache manager
* @param command implementation of the {@link TagsCommand} * @param command implementation of the {@link TagsCommand}
* @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) {
logger.debug("get tags for repository {}", repository);
if (tags == null)
{
if (logger.isDebugEnabled())
{
logger.debug("get tags for repository {}", repository);
}
tags = getTagsFromCommand(); tags = getTagsFromCommand();
cache.put(key, tags);
if (tags != null) } else {
{ logger.debug("get tags for repository {} from cache", repository);
cache.put(key, tags);
}
}
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;
} }
@@ -190,17 +175,15 @@ public final class TagsCommandBuilder
return Objects.equal(repositoryId, other.repositoryId); return Objects.equal(repositoryId, other.repositoryId);
} }
@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;
} }

View File

@@ -21,19 +21,24 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
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());
}
} }

View File

@@ -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;

View File

@@ -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))

View File

@@ -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();

View File

@@ -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(
name, link,
revision: changeset.id, {
}) name,
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) {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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,9 +98,12 @@ 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 =
<SignatureIcon className="mx-2" signatures={changeset.signatures} /> changeset?.signatures && changeset.signatures.length > 0 ? (
); <SignatureIcon className="mx-2" signatures={changeset.signatures} />
) : (
<>&nbsp;</>
);
if (open) { if (open) {
return ( return (
@@ -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")} /> <ChangesetAuthor changeset={changeset} />
<Icon name="angle-right" alt={t("changeset.contributors.showList")} />{" "} </ContributorColumn>
<ChangesetAuthor changeset={changeset} /> {signatureIcon}
</ContributorColumn> <CountColumn className="is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only">
{signatureIcon} (<span>{t("changeset.contributors.count", { count: countContributors(changeset) })}</span>)
<CountColumn className="is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only"> </CountColumn>
( </div>
<span className="has-text-link"> );
{t("changeset.contributors.count", { count: countContributors(changeset) })} };
</span>
) const ContainedInTags: FC<{ changeset: Changeset; repository: Repository }> = ({ changeset, repository }) => {
</CountColumn> 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> </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]} />

View File

@@ -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);
} }
} }
} }

View File

@@ -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()))));

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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",

View File

@@ -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";
when(logCommandBuilder.getChangeset(id)).thenReturn( private final String id = "revision_123";
new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit) 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";
MockHttpRequest request = MockHttpRequest private final JsonMockHttpResponse response = new JsonMockHttpResponse();
.get(CHANGESET_URL + id)
.accept(VndMediaType.CHANGESET);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus()); @BeforeEach
assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); void prepareExistingChangeset() throws URISyntaxException, IOException {
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); when(logCommandBuilder.getChangeset(id)).thenReturn(
assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); );
}
private void executeRequest() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest
.get(CHANGESET_URL + id)
.accept(VndMediaType.CHANGESET);
dispatcher.invoke(request, response);
}
@Test
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 @Test
public void shouldReturnNotFoundForNonExistingChangeset() throws Exception { void shouldReturnNotFoundForNonExistingChangeset() throws Exception {
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(CHANGESET_URL + "abcd") .get(CHANGESET_URL + "abcd")
.accept(VndMediaType.CHANGESET); .accept(VndMediaType.CHANGESET);

View File

@@ -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)));
} }