Merge branch 'develop' into feature/import_git_from_url

This commit is contained in:
Eduard Heimbuch
2020-12-02 14:39:45 +01:00
71 changed files with 2240 additions and 588 deletions

View File

@@ -55,6 +55,8 @@ import org.eclipse.jgit.util.LfsFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import sonia.scm.web.GitUserAgentProvider;
@@ -63,6 +65,7 @@ import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -73,73 +76,37 @@ import static java.util.Optional.of;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public final class GitUtil
{
public final class GitUtil {
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
/** Field description */
public static final String REF_HEAD = "HEAD";
/** Field description */
public static final String REF_HEAD_PREFIX = "refs/heads/";
/** Field description */
public static final String REF_MASTER = "master";
/** Field description */
private static final String DIRECTORY_DOTGIT = ".git";
/** Field description */
private static final String DIRECTORY_OBJETCS = "objects";
/** Field description */
private static final String DIRECTORY_REFS = "refs";
/** Field description */
private static final String PREFIX_HEADS = "refs/heads/";
/** Field description */
private static final String PREFIX_TAG = "refs/tags/";
/** Field description */
private static final String REFSPEC = "+refs/heads/*:refs/remote/scm/%s/*";
/** Field description */
private static final String REMOTE_REF = "refs/remote/scm/%s/%s";
/** Field description */
private static final int TIMEOUT = 5;
/** Field description */
private static final String USERAGENT_GIT = "git/";
/** the logger for GitUtil */
/**
* the logger for GitUtil
*/
private static final Logger logger = LoggerFactory.getLogger(GitUtil.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
private GitUtil() {}
private GitUtil() {
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param repo
*/
public static void close(org.eclipse.jgit.lib.Repository repo)
{
if (repo != null)
{
public static void close(org.eclipse.jgit.lib.Repository repo) {
if (repo != null) {
repo.close();
}
}
@@ -147,42 +114,30 @@ public final class GitUtil
/**
* TODO cache
*
*
* @param repository
* @param revWalk
*
*
* @return
*/
public static Multimap<ObjectId,
String> createTagMap(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk)
{
RevWalk revWalk) {
Multimap<ObjectId, String> tags = ArrayListMultimap.create();
Map<String, Ref> tagMap = repository.getTags();
if (tagMap != null)
{
for (Map.Entry<String, Ref> e : tagMap.entrySet())
{
try
{
if (tagMap != null) {
for (Map.Entry<String, Ref> e : tagMap.entrySet()) {
try {
RevCommit c = getCommit(repository, revWalk, e.getValue());
if (c != null)
{
if (c != null) {
tags.put(c.getId(), e.getKey());
}
else if (logger.isWarnEnabled())
{
} else {
logger.warn("could not find commit for tag {}", e.getKey());
}
}
catch (IOException ex)
{
} catch (IOException ex) {
logger.error("could not read commit for ref", ex);
}
@@ -193,8 +148,7 @@ public final class GitUtil
}
public static FetchResult fetch(Git git, File directory, Repository remoteRepository) {
try
{
try {
FetchCommand fetch = git.fetch();
fetch.setRemote(directory.getAbsolutePath());
@@ -202,123 +156,63 @@ public final class GitUtil
fetch.setTimeout((int) TimeUnit.MINUTES.toSeconds(TIMEOUT));
return fetch.call();
}
catch (GitAPIException ex)
{
} catch (GitAPIException ex) {
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Remote", directory.toString()).in(remoteRepository), "could not fetch", ex);
}
}
/**
* Method description
*
*
* @param directory
*
* @return
*
* @throws IOException
*/
public static org.eclipse.jgit.lib.Repository open(File directory)
throws IOException
{
throws IOException {
FS fs = FS.DETECTED;
FileRepositoryBuilder builder = new FileRepositoryBuilder();
builder.setFS(fs);
if (isGitDirectory(fs, directory))
{
if (isGitDirectory(fs, directory)) {
// bare repository
builder.setGitDir(directory).setBare();
}
else
{
} else {
builder.setWorkTree(directory);
}
return builder.build();
}
/**
* Method description
*
*
* @param formatter
*/
public static void release(DiffFormatter formatter)
{
if (formatter != null)
{
public static void release(DiffFormatter formatter) {
if (formatter != null) {
formatter.close();
}
}
/**
* Method description
*
*
* @param walk
*/
public static void release(TreeWalk walk)
{
if (walk != null)
{
public static void release(TreeWalk walk) {
if (walk != null) {
walk.close();
}
}
/**
* Method description
*
*
* @param walk
*/
public static void release(RevWalk walk)
{
if (walk != null)
{
public static void release(RevWalk walk) {
if (walk != null) {
walk.close();
}
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param ref
*
* @return
*/
public static String getBranch(Ref ref)
{
public static String getBranch(Ref ref) {
String branch = null;
if (ref != null)
{
if (ref != null) {
branch = getBranch(ref.getName());
}
return branch;
}
/**
* Method description
*
*
* @param name
*
* @return
*/
public static String getBranch(String name)
{
public static String getBranch(String name) {
String branch = null;
if (Util.isNotEmpty(name) && name.startsWith(PREFIX_HEADS))
{
if (Util.isNotEmpty(name) && name.startsWith(PREFIX_HEADS)) {
branch = name.substring(PREFIX_HEADS.length());
}
@@ -329,18 +223,15 @@ public final class GitUtil
* Returns {@code true} if the provided reference name is a branch name.
*
* @param refName reference name
*
* @return {@code true} if the name is a branch name
*
* @since 1.50
*/
public static boolean isBranch(String refName)
{
public static boolean isBranch(String refName) {
return Strings.nullToEmpty(refName).startsWith(PREFIX_HEADS);
}
public static Ref getBranchIdOrCurrentHead(org.eclipse.jgit.lib.Repository gitRepository, String requestedBranch) throws IOException {
if ( Strings.isNullOrEmpty(requestedBranch) ) {
if (Strings.isNullOrEmpty(requestedBranch)) {
logger.trace("no default branch configured, use repository head as default");
Optional<Ref> repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository);
return repositoryHeadRef.orElse(null);
@@ -352,37 +243,28 @@ public final class GitUtil
/**
* Method description
*
*
* @param repo
* @param branchName
*
* @return
*
* @throws IOException
*/
public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo,
String branchName)
throws IOException
{
String branchName)
throws IOException {
Ref ref = null;
if (!branchName.startsWith(REF_HEAD))
{
if (!branchName.startsWith(REF_HEAD)) {
branchName = PREFIX_HEADS.concat(branchName);
}
checkBranchName(repo, branchName);
try
{
try {
ref = repo.findRef(branchName);
if (ref == null)
{
if (ref == null) {
logger.warn("could not find branch for {}", branchName);
}
}
catch (IOException ex)
{
} catch (IOException ex) {
logger.warn("error occured during resolve of branch id", ex);
}
@@ -415,33 +297,21 @@ public final class GitUtil
}
/**
* Method description
*
*
* @param repository
* @param revWalk
* @param ref
*
* @return
*
* @throws IOException
* Returns the commit for the given ref.
* If the given ref is for a tag, the commit that this tag belongs to is returned instead.
*/
public static RevCommit getCommit(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk, Ref ref)
throws IOException
{
RevWalk revWalk, Ref ref)
throws IOException {
RevCommit commit = null;
ObjectId id = ref.getPeeledObjectId();
if (id == null)
{
if (id == null) {
id = ref.getObjectId();
}
if (id != null)
{
if (revWalk == null)
{
if (id != null) {
if (revWalk == null) {
revWalk = new RevWalk(repository);
}
@@ -451,16 +321,30 @@ public final class GitUtil
return commit;
}
public static RevTag getTag(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk, Ref ref)
throws IOException {
RevTag tag = null;
ObjectId id = ref.getObjectId();
if (id != null) {
if (revWalk == null) {
revWalk = new RevWalk(repository);
}
tag = revWalk.parseTag(id);
}
return tag;
}
/**
* Method description
*
*
* @param commit
*
* @return
*/
public static long getCommitTime(RevCommit commit)
{
public static long getCommitTime(RevCommit commit) {
long date = commit.getCommitTime();
date = date * 1000;
@@ -471,17 +355,13 @@ public final class GitUtil
/**
* Method description
*
*
* @param objectId
*
* @return
*/
public static String getId(AnyObjectId objectId)
{
public static String getId(AnyObjectId objectId) {
String id = Util.EMPTY_STRING;
if (objectId != null)
{
if (objectId != null) {
id = objectId.name();
}
@@ -491,44 +371,27 @@ public final class GitUtil
/**
* Method description
*
*
* @param repository
* @param id
*
* @return
*
* @throws IOException
*/
public static Ref getRefForCommit(org.eclipse.jgit.lib.Repository repository,
ObjectId id)
throws IOException
{
ObjectId id)
throws IOException {
Ref ref = null;
RevWalk walk = null;
try
{
walk = new RevWalk(repository);
try (RevWalk walk = new RevWalk(repository)) {
RevCommit commit = walk.parseCommit(id);
for (Map.Entry<String, Ref> e : repository.getAllRefs().entrySet())
{
if (e.getKey().startsWith(Constants.R_HEADS))
{
if (walk.isMergedInto(commit,
walk.parseCommit(e.getValue().getObjectId())))
{
ref = e.getValue();
}
for (Map.Entry<String, Ref> e : repository.getAllRefs().entrySet()) {
if (e.getKey().startsWith(Constants.R_HEADS) && walk.isMergedInto(commit,
walk.parseCommit(e.getValue().getObjectId()))) {
ref = e.getValue();
}
}
}
finally
{
release(walk);
}
return ref;
}
@@ -580,26 +443,19 @@ public final class GitUtil
/**
* Method description
*
*
* @param repo
* @param revision
*
* @return
*
* @throws IOException
*/
public static ObjectId getRevisionId(org.eclipse.jgit.lib.Repository repo,
String revision)
throws IOException
{
String revision)
throws IOException {
ObjectId revId;
if (Util.isNotEmpty(revision))
{
if (Util.isNotEmpty(revision)) {
revId = repo.resolve(revision);
}
else
{
} else {
revId = getRepositoryHead(repo);
}
@@ -609,34 +465,27 @@ public final class GitUtil
/**
* Method description
*
*
* @param repository
* @param localBranch
*
* @return
*/
public static String getScmRemoteRefName(Repository repository,
Ref localBranch)
{
Ref localBranch) {
return getScmRemoteRefName(repository, localBranch.getName());
}
/**
* Method description
*
*
* @param repository
* @param localBranch
*
* @return
*/
public static String getScmRemoteRefName(Repository repository,
String localBranch)
{
String localBranch) {
String branch = localBranch;
if (localBranch.startsWith(REF_HEAD_PREFIX))
{
if (localBranch.startsWith(REF_HEAD_PREFIX)) {
branch = localBranch.substring(REF_HEAD_PREFIX.length());
}
@@ -647,16 +496,12 @@ public final class GitUtil
* Returns the name of the tag or {@code null} if the the ref is not a tag.
*
* @param refName ref name
*
* @return name of tag or {@link null}
*
* @since 1.50
*/
public static String getTagName(String refName)
{
public static String getTagName(String refName) {
String tagName = null;
if (refName.startsWith(PREFIX_TAG))
{
if (refName.startsWith(PREFIX_TAG)) {
tagName = refName.substring(PREFIX_TAG.length());
}
@@ -666,91 +511,112 @@ public final class GitUtil
/**
* Method description
*
*
* @param ref
*
* @return
*/
public static String getTagName(Ref ref)
{
public static String getTagName(Ref ref) {
String name = ref.getName();
if (name.startsWith(PREFIX_TAG))
{
if (name.startsWith(PREFIX_TAG)) {
name = name.substring(PREFIX_TAG.length());
}
return name;
}
private static final String GPG_HEADER = "-----BEGIN PGP SIGNATURE-----";
public static Optional<Signature> getTagSignature(RevObject revObject, GPG gpg, RevWalk revWalk) throws IOException {
if (revObject instanceof RevTag) {
final byte[] messageBytes = revWalk.getObjectReader().open(revObject.getId()).getBytes();
final String message = new String(messageBytes);
final int signatureStartIndex = message.indexOf(GPG_HEADER);
if (signatureStartIndex < 0) {
return Optional.empty();
}
final String signature = message.substring(signatureStartIndex);
String publicKeyId = gpg.findPublicKeyId(signature.getBytes());
if (Strings.isNullOrEmpty(publicKeyId)) {
// key not found
return Optional.of(new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()));
}
Optional<PublicKey> publicKeyById = gpg.findPublicKey(publicKeyId);
if (!publicKeyById.isPresent()) {
// key not found
return Optional.of(new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()));
}
PublicKey publicKey = publicKeyById.get();
String rawMessage = message.substring(0, signatureStartIndex);
boolean verified = publicKey.verify(rawMessage.getBytes(), signature.getBytes());
return Optional.of(new Signature(
publicKeyId,
"gpg",
verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID,
publicKey.getOwner().orElse(null),
publicKey.getContacts()
));
}
return Optional.empty();
}
/**
* Returns true if the request comes from a git client.
*
*
* @param request servlet request
*
* @return true if the client is git
*/
public static boolean isGitClient(HttpServletRequest request)
{
public static boolean isGitClient(HttpServletRequest request) {
return GIT_USER_AGENT_PROVIDER.parseUserAgent(request.getHeader(HttpUtil.HEADER_USERAGENT)) != null;
}
/**
* Method description
*
*
* @param dir
*
* @return
*/
public static boolean isGitDirectory(File dir)
{
public static boolean isGitDirectory(File dir) {
return isGitDirectory(FS.DETECTED, dir);
}
/**
* Method description
*
*
* @param fs
* @param dir
*
* @return
*/
public static boolean isGitDirectory(FS fs, File dir)
{
public static boolean isGitDirectory(FS fs, File dir) {
//J-
return fs.resolve(dir, DIRECTORY_OBJETCS).exists()
&& fs.resolve(dir, DIRECTORY_REFS).exists()
&&!fs.resolve(dir, DIRECTORY_DOTGIT).exists();
&& !fs.resolve(dir, DIRECTORY_DOTGIT).exists();
//J+
}
/**
* Method description
*
*
* @param ref
*
* @return
*/
public static boolean isHead(String ref)
{
public static boolean isHead(String ref) {
return ref.startsWith(REF_HEAD_PREFIX);
}
/**
* Method description
*
*
* @param id
*
* @return
*/
public static boolean isValidObjectId(ObjectId id)
{
return (id != null) &&!id.equals(ObjectId.zeroId());
public static boolean isValidObjectId(ObjectId id) {
return (id != null) && !id.equals(ObjectId.zeroId());
}
/**
@@ -792,25 +658,20 @@ public final class GitUtil
/**
* Method description
*
*
* @param repo
* @param branchName
*
* @throws IOException
*/
@VisibleForTesting
static void checkBranchName(org.eclipse.jgit.lib.Repository repo,
String branchName)
throws IOException
{
if (branchName.contains(".."))
{
String branchName)
throws IOException {
if (branchName.contains("..")) {
File repoDirectory = repo.getDirectory();
File branchFile = new File(repoDirectory, branchName);
if (!branchFile.getCanonicalPath().startsWith(
repoDirectory.getCanonicalPath()))
{
repoDirectory.getCanonicalPath())) {
logger.error(
"branch \"{}\" is outside of the repository. It looks like path traversal attack",
branchName);
@@ -824,13 +685,10 @@ public final class GitUtil
/**
* Method description
*
*
* @param repository
*
* @return
*/
private static RefSpec createRefSpec(Repository repository)
{
private static RefSpec createRefSpec(Repository repository) {
return new RefSpec(String.format(REFSPEC, repository.getId()));
}
}

View File

@@ -47,6 +47,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
Command.DIFF,
Command.DIFF_RESULT,
Command.LOG,
Command.TAG,
Command.TAGS,
Command.BRANCH,
Command.BRANCHES,
@@ -142,7 +143,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
@Override
public TagsCommand getTagsCommand() {
return new GitTagsCommand(context);
return commandInjector.getInstance(GitTagsCommand.class);
}
@Override
public TagCommand getTagCommand() {
return commandInjector.getInstance(GitTagCommand.class);
}
@Override

View File

@@ -0,0 +1,214 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import com.google.common.base.Strings;
import org.apache.shiro.SecurityUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookTagProvider;
import sonia.scm.repository.api.TagCreateRequest;
import sonia.scm.repository.api.TagDeleteRequest;
import sonia.scm.user.User;
import javax.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class GitTagCommand extends AbstractGitCommand implements TagCommand {
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
@Inject
GitTagCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
super(context);
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
}
@Override
public Tag create(TagCreateRequest request) {
final String name = request.getName();
final String revision = request.getRevision();
if (Strings.isNullOrEmpty(revision)) {
throw new IllegalArgumentException("Revision is required");
}
if (Strings.isNullOrEmpty(name)) {
throw new IllegalArgumentException("Name is required");
}
try (Git git = new Git(context.open())) {
RevObject revObject;
Long tagTime;
ObjectId taggedCommitObjectId = git.getRepository().resolve(revision);
if (taggedCommitObjectId == null) {
throw notFound(entity("revision", revision).in(repository));
}
try (RevWalk walk = new RevWalk(git.getRepository())) {
revObject = walk.parseAny(taggedCommitObjectId);
tagTime = GitUtil.getTagTime(walk, taggedCommitObjectId);
}
Tag tag = new Tag(name, revision, tagTime);
RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.createHookEvent(tag));
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
User user = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
PersonIdent taggerIdent = new PersonIdent(user.getDisplayName(), user.getMail());
git.tag()
.setObjectId(revObject)
.setTagger(taggerIdent)
.setName(name)
.call();
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
return tag;
} catch (IOException | GitAPIException ex) {
throw new InternalRepositoryException(repository, "could not create tag " + name + " for revision " + revision, ex);
}
}
@Override
public void delete(TagDeleteRequest request) {
String name = request.getName();
if (Strings.isNullOrEmpty(name)) {
throw new IllegalArgumentException("Name is required");
}
try (Git git = new Git(context.open())) {
final Repository repository = git.getRepository();
Optional<Ref> tagRef = findTagRef(git, name);
Tag tag;
// Deleting a non-existent tag is a valid action and simply succeeds without
// anything happening.
if (!tagRef.isPresent()) {
return;
}
try (RevWalk walk = new RevWalk(repository)) {
final RevCommit commit = GitUtil.getCommit(repository, walk, tagRef.get());
Long tagTime = GitUtil.getTagTime(walk, tagRef.get().getObjectId());
tag = new Tag(name, commit.name(), tagTime);
}
RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.deleteHookEvent(tag));
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
git.tagDelete().setTags(name).call();
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
} catch (GitAPIException | IOException e) {
throw new InternalRepositoryException(repository, "could not delete tag " + name, e);
}
}
private Optional<Ref> findTagRef(Git git, String name) throws GitAPIException {
final String tagRef = "refs/tags/" + name;
return git.tagList().call().stream().filter(it -> it.getName().equals(tagRef)).findAny();
}
private RepositoryHookEvent createTagHookEvent(TagHookContextProvider hookEvent) {
HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository());
return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE);
}
private static class TagHookContextProvider extends HookContextProvider {
private final List<Tag> newTags;
private final List<Tag> deletedTags;
private TagHookContextProvider(List<Tag> newTags, List<Tag> deletedTags) {
this.newTags = newTags;
this.deletedTags = deletedTags;
}
static TagHookContextProvider createHookEvent(Tag newTag) {
return new TagHookContextProvider(singletonList(newTag), emptyList());
}
static TagHookContextProvider deleteHookEvent(Tag deletedTag) {
return new TagHookContextProvider(emptyList(), singletonList(deletedTag));
}
@Override
public Set<HookFeature> getSupportedFeatures() {
return singleton(HookFeature.TAG_PROVIDER);
}
@Override
public HookTagProvider getTagProvider() {
return new HookTagProvider() {
@Override
public List<Tag> getCreatedTags() {
return newTags;
}
@Override
public List<Tag> getDeletedTags() {
return deletedTags;
}
};
}
@Override
public HookChangesetProvider getChangesetProvider() {
return r -> new HookChangesetResponse(emptyList());
}
}
}

View File

@@ -30,15 +30,21 @@ import com.google.common.base.Function;
import com.google.common.collect.Lists;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Tag;
import sonia.scm.security.GPG;
import javax.inject.Inject;
import java.io.IOException;
import java.util.List;
@@ -49,32 +55,34 @@ import java.util.List;
*/
public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
private final GPG gpg;
/**
* Constructs ...
*
* @param context
*/
public GitTagsCommand(GitContext context) {
@Inject
public GitTagsCommand(GitContext context, GPG gpp) {
super(context);
this.gpg = gpp;
}
//~--- get methods ----------------------------------------------------------
@Override
public List<Tag> getTags() throws IOException {
List<Tag> tags = null;
List<Tag> tags;
RevWalk revWalk = null;
try {
final Git git = new Git(open());
try (Git git = new Git(open())) {
revWalk = new RevWalk(git.getRepository());
List<Ref> tagList = git.tagList().call();
tags = Lists.transform(tagList,
new TransformFuntion(git.getRepository(), revWalk));
new TransformFunction(git.getRepository(), revWalk, gpg));
} catch (GitAPIException ex) {
throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
} finally {
@@ -92,26 +100,27 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
* @author Enter your name here...
* @version Enter version here..., 12/07/06
*/
private static class TransformFuntion implements Function<Ref, Tag> {
private static class TransformFunction implements Function<Ref, Tag> {
/**
* the logger for TransformFuntion
*/
private static final Logger logger =
LoggerFactory.getLogger(TransformFuntion.class);
LoggerFactory.getLogger(TransformFunction.class);
//~--- constructors -------------------------------------------------------
/**
* Constructs ...
*
* @param repository
* @param repository
* @param revWalk
*/
public TransformFuntion(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk) {
public TransformFunction(Repository repository,
RevWalk revWalk,
GPG gpg) {
this.repository = repository;
this.revWalk = revWalk;
this.gpg = gpg;
}
//~--- methods ------------------------------------------------------------
@@ -127,14 +136,17 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
Tag tag = null;
try {
RevObject revObject = GitUtil.getCommit(repository, revWalk, ref);
if (revObject != null) {
RevCommit revCommit = GitUtil.getCommit(repository, revWalk, ref);
if (revCommit != null) {
String name = GitUtil.getTagName(ref);
tag = new Tag(name, revObject.getId().name(), GitUtil.getTagTime(revWalk, ref.getObjectId()));
tag = new Tag(name, revCommit.getId().name(), GitUtil.getTagTime(revWalk, ref.getObjectId()));
RevObject revObject = revWalk.parseAny(ref.getObjectId());
if (revObject.getType() == Constants.OBJ_TAG) {
RevTag revTag = (RevTag) revObject;
GitUtil.getTagSignature(revTag, gpg, revWalk)
.ifPresent(tag::addSignature);
}
}
} catch (IOException ex) {
logger.error("could not get commit for tag", ex);
}
@@ -147,11 +159,12 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
/**
* Field description
*/
private org.eclipse.jgit.lib.Repository repository;
private final org.eclipse.jgit.lib.Repository repository;
/**
* Field description
*/
private RevWalk revWalk;
private final RevWalk revWalk;
private final GPG gpg;
}
}

View File

@@ -32,7 +32,6 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.api.BranchRequest;

View File

@@ -0,0 +1,170 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.eclipse.jgit.lib.GpgSigner;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.TagDeleteRequest;
import sonia.scm.repository.api.TagCreateRequest;
import sonia.scm.security.GPG;
import sonia.scm.util.MockUtil;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class GitTagCommandTest extends AbstractGitCommandTestBase {
@Mock
private GPG gpg;
@Mock
private HookContextFactory hookContextFactory;
@Mock
private ScmEventBus eventBus;
private Subject subject;
@Before
public void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner());
}
@Before
public void bindThreadContext() {
SecurityUtils.setSecurityManager(new DefaultSecurityManager());
subject = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
ThreadContext.bind(subject);
}
@After
public void unbindThreadContext() {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
@Test
public void shouldCreateATag() throws IOException {
createCommand().create(new TagCreateRequest("592d797cd36432e591416e8b2b98154f4f163411", "newtag"));
Optional<Tag> optionalTag = findTag(createContext(), "newtag");
assertThat(optionalTag).isNotEmpty();
final Tag tag = optionalTag.get();
assertThat(tag.getName()).isEqualTo("newtag");
assertThat(tag.getRevision()).isEqualTo("592d797cd36432e591416e8b2b98154f4f163411");
}
@Test
public void shouldPostCreateEvent() {
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
createCommand().create(new TagCreateRequest("592d797cd36432e591416e8b2b98154f4f163411", "newtag"));
List<Object> events = captor.getAllValues();
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getTagProvider().getCreatedTags().get(0).getName()).isEqualTo("newtag");
assertThat(event.getContext().getTagProvider().getDeletedTags()).isEmpty();
}
@Test
public void shouldDeleteATag() throws IOException {
final GitContext context = createContext();
Optional<Tag> tag = findTag(context, "test-tag");
assertThat(tag).isNotEmpty();
createCommand().delete(new TagDeleteRequest("test-tag"));
tag = findTag(context, "test-tag");
assertThat(tag).isEmpty();
}
@Test
public void shouldPostDeleteEvent() {
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
createCommand().delete(new TagDeleteRequest("test-tag"));
List<Object> events = captor.getAllValues();
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getTagProvider().getCreatedTags()).isEmpty();
final Tag deletedTag = event.getContext().getTagProvider().getDeletedTags().get(0);
assertThat(deletedTag.getName()).isEqualTo("test-tag");
assertThat(deletedTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
}
private GitTagCommand createCommand() {
return new GitTagCommand(createContext(), hookContextFactory, eventBus);
}
private List<Tag> readTags(GitContext context) throws IOException {
return new GitTagsCommand(context, gpg).getTags();
}
private Optional<Tag> findTag(GitContext context, String name) throws IOException {
List<Tag> tags = readTags(context);
return tags.stream().filter(t -> name.equals(t.getName())).findFirst();
}
private HookContext createMockedContext(InvocationOnMock invocation) {
HookContext mock = mock(HookContext.class);
when(mock.getTagProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getTagProvider());
return mock;
}
}

View File

@@ -24,48 +24,86 @@
package sonia.scm.repository.spi;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.SignatureStatus;
import sonia.scm.repository.Tag;
import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
@RunWith(MockitoJUnitRunner.class)
public class GitTagsCommandTest extends AbstractGitCommandTestBase {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Mock
GPG gpg;
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Rule
public ShiroRule shiro = new ShiroRule();
@Mock
PublicKey publicKey;
@Test
public void shouldGetDatesCorrectly() throws IOException {
final GitContext gitContext = createContext();
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext);
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, gpg);
final List<Tag> tags = tagsCommand.getTags();
assertThat(tags).hasSize(2);
assertThat(tags).hasSize(3);
Tag annotatedTag = tags.get(0);
assertThat(annotatedTag.getName()).isEqualTo("1.0.0");
assertThat(annotatedTag.getDate()).contains(1598348105000L); // Annotated - Take tag date
assertThat(annotatedTag.getRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
Tag lightweightTag = tags.get(1);
Tag lightweightTag = tags.get(2);
assertThat(lightweightTag.getName()).isEqualTo("test-tag");
assertThat(lightweightTag.getDate()).contains(1339416344000L); // Lightweight - Take commit date
assertThat(lightweightTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
}
@Test
public void shouldGetSignatures() throws IOException {
when(gpg.findPublicKeyId(ArgumentMatchers.any())).thenReturn("2BA27721F113C005CC16F06BAE63EFBC49F140CF");
when(gpg.findPublicKey("2BA27721F113C005CC16F06BAE63EFBC49F140CF")).thenReturn(Optional.of(publicKey));
String signature = "-----BEGIN PGP SIGNATURE-----\n" +
"\n" +
"iQEzBAABCgAdFiEEK6J3IfETwAXMFvBrrmPvvEnxQM8FAl+9acoACgkQrmPvvEnx\n" +
"QM9abwgAnGP+Y/Ijli+PAsimfOmZQWYepjptoOv9m7i3bnHv8V+Qg6cm51I3E0YV\n" +
"R2QaxxzW9PgS4hcES+L1qs8Lwo18RurF469eZEmNb8DcUFJ3sEWeHlIl5wZNNo/v\n" +
"jJm0d9LNcSmtAIiQ8eDMoGdFXJzHewGickLOSsQGmfZgZus4Qlsh7r3BZTI1Zwd/\n" +
"6jaBFctX13FuepCTxq2SjEfRaQHIYkyFQq2o6mjL5S2qfYJ/S//gcCCzxllQrisF\n" +
"5fRW3LzLI4eXFH0vua7+UzNS2Rwpifg2OENJA/Kn+3R36LWEGxFK9pNqjVPRAcQj\n" +
"1vSkcjK26RqhAqCjNLSagM8ATZrh+g==\n" +
"=kUKm\n" +
"-----END PGP SIGNATURE-----\n";
String signedContent = "object 592d797cd36432e591416e8b2b98154f4f163411\n" +
"type commit\n" +
"tag signedtag\n" +
"tagger Arthur Dent <arthur.dent@hitchhiker.com> 1606248906 +0100\n" +
"\n" +
"this tag is signed\n";
when(publicKey.verify(signedContent.getBytes(), signature.getBytes())).thenReturn(true);
final GitContext gitContext = createContext();
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, gpg);
final List<Tag> tags = tagsCommand.getTags();
assertThat(tags).hasSize(3);
Tag signedTag = tags.get(1);
assertThat(signedTag.getSignatures()).isNotEmpty();
assertThat(signedTag.getSignatures().get(0).getStatus()).isEqualTo(SignatureStatus.VERIFIED);
}
@Override
protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-spi-test-tags.zip";