Feature/mirror (#1683)

Add mirror command and extension points.

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-06-04 14:05:47 +02:00
committed by GitHub
parent e55ba52ace
commit dd0975b49a
111 changed files with 6018 additions and 796 deletions

View File

@@ -0,0 +1,44 @@
/*
* 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.protocolcommand.git;
import org.eclipse.jgit.lib.Ref;
import sonia.scm.repository.spi.GitMirrorCommand;
import java.util.Map;
import java.util.stream.Collectors;
final class MirrorRefFilter {
private MirrorRefFilter() {
}
static Map<String, Ref> filterMirrors(Map<String, Ref> refs) {
return refs.entrySet()
.stream()
.filter(entry -> !entry.getKey().startsWith(GitMirrorCommand.MIRROR_REF_PREFIX))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.protocolcommand.git;
import org.eclipse.jgit.lib.Repository;
@@ -32,6 +32,8 @@ import sonia.scm.protocolcommand.RepositoryContext;
public class ScmUploadPackFactory implements UploadPackFactory<RepositoryContext> {
@Override
public UploadPack create(RepositoryContext repositoryContext, Repository repository) {
return new UploadPack(repository);
UploadPack uploadPack = new UploadPack(repository);
uploadPack.setRefFilter(MirrorRefFilter::filterMirrors);
return uploadPack;
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.protocolcommand.git;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.UploadPack;
import org.eclipse.jgit.transport.resolver.UploadPackFactory;
import javax.servlet.http.HttpServletRequest;
public class ScmUploadPackFactoryForHttpServletRequest implements UploadPackFactory<HttpServletRequest> {
@Override
public UploadPack create(HttpServletRequest repositoryContext, Repository repository) {
UploadPack uploadPack = new UploadPack(repository);
uploadPack.setRefFilter(MirrorRefFilter::filterMirrors);
return uploadPack;
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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;
import com.google.common.collect.ImmutableMap;
import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import java.util.Map;
import java.util.Optional;
class GPGSignatureResolver {
private final GPG gpg;
private final Map<String, PublicKey> additionalPublicKeys;
GPGSignatureResolver(GPG gpg, Iterable<PublicKey> additionalPublicKeys) {
this.gpg = gpg;
this.additionalPublicKeys = createKeyMap(additionalPublicKeys);
}
private Map<String, PublicKey> createKeyMap(Iterable<PublicKey> additionalPublicKeys) {
ImmutableMap.Builder<String, PublicKey> builder = ImmutableMap.builder();
for (PublicKey key : additionalPublicKeys) {
appendKey(builder, key);
}
return builder.build();
}
private void appendKey(ImmutableMap.Builder<String, PublicKey> builder, PublicKey key) {
builder.put(key.getId(), key);
for (String subkey : key.getSubkeys()) {
builder.put(subkey, key);
}
}
String findPublicKeyId(byte[] signature) {
return gpg.findPublicKeyId(signature);
}
Optional<PublicKey> findPublicKey(String id) {
PublicKey publicKey = additionalPublicKeys.get(id);
if (publicKey != null) {
return Optional.of(publicKey);
}
return gpg.findPublicKey(id);
}
}

View File

@@ -35,7 +35,6 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.RawParseUtils;
import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import sonia.scm.util.Util;
@@ -56,11 +55,11 @@ import java.util.Optional;
*/
public class GitChangesetConverter implements Closeable {
private final GPG gpg;
private final GPGSignatureResolver gpg;
private final Multimap<ObjectId, String> tags;
private final TreeWalk treeWalk;
public GitChangesetConverter(GPG gpg, org.eclipse.jgit.lib.Repository repository, RevWalk revWalk) {
GitChangesetConverter(GPGSignatureResolver gpg, org.eclipse.jgit.lib.Repository repository, RevWalk revWalk) {
this.gpg = gpg;
this.tags = GitUtil.createTagMap(repository, revWalk);
this.treeWalk = new TreeWalk(repository);

View File

@@ -27,8 +27,13 @@ package sonia.scm.repository;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class GitChangesetConverterFactory {
@@ -40,11 +45,52 @@ public class GitChangesetConverterFactory {
}
public GitChangesetConverter create(Repository repository) {
return new GitChangesetConverter(gpg, repository, new RevWalk(repository));
return builder(repository).create();
}
public GitChangesetConverter create(Repository repository, RevWalk revWalk) {
return new GitChangesetConverter(gpg, repository, revWalk);
return builder(repository).withRevWalk(revWalk).create();
}
public Builder builder(Repository repository) {
return new Builder(gpg, repository);
}
public static class Builder {
private final GPG gpg;
private final Repository repository;
private RevWalk revWalk;
private final List<PublicKey> additionalPublicKeys = new ArrayList<>();
private Builder(GPG gpg, Repository repository) {
this.gpg = gpg;
this.repository = repository;
}
public Builder withRevWalk(RevWalk revWalk) {
this.revWalk = revWalk;
return this;
}
public Builder withAdditionalPublicKeys(PublicKey... publicKeys) {
additionalPublicKeys.addAll(Arrays.asList(publicKeys));
return this;
}
public Builder withAdditionalPublicKeys(Collection<PublicKey> publicKeys) {
additionalPublicKeys.addAll(publicKeys);
return this;
}
public GitChangesetConverter create() {
return new GitChangesetConverter(
new GPGSignatureResolver(gpg, additionalPublicKeys),
repository,
revWalk != null ? revWalk : new RevWalk(repository)
);
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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;
import org.eclipse.jgit.transport.HttpTransport;
import sonia.scm.web.ScmHttpConnectionFactory;
import sonia.scm.plugin.Extension;
import javax.inject.Inject;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
@Extension
public class GitHttpTransportRegistration implements ServletContextListener {
private final ScmHttpConnectionFactory connectionFactory;
@Inject
public GitHttpTransportRegistration(ScmHttpConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
// Override default http connection factory to inject our own ssl context
HttpTransport.setConnectionFactory(connectionFactory);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
// Nothing to destroy
}
}

View File

@@ -49,6 +49,7 @@ import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.TagOpt;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.LfsFactory;
@@ -97,6 +98,7 @@ public final class GitUtil {
* the logger for GitUtil
*/
private static final Logger logger = LoggerFactory.getLogger(GitUtil.class);
private static final String REF_SPEC = "refs/heads/*:refs/heads/*";
//~--- constructors ---------------------------------------------------------
@@ -691,4 +693,10 @@ public final class GitUtil {
private static RefSpec createRefSpec(Repository repository) {
return new RefSpec(String.format(REFSPEC, repository.getId()));
}
public static FetchCommand createFetchCommandWithBranchAndTagUpdate(Git git) {
return git.fetch()
.setRefSpecs(new RefSpec(REF_SPEC))
.setTagOpt(TagOpt.FETCH_TAGS);
}
}

View File

@@ -0,0 +1,567 @@
/*
* 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.Stopwatch;
import com.google.common.base.Strings;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
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.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.TrackingRefUpdate;
import org.eclipse.jgit.transport.TransportHttp;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.MirrorCommandResult;
import sonia.scm.repository.api.MirrorCommandResult.ResultType;
import sonia.scm.repository.api.MirrorFilter;
import sonia.scm.repository.api.MirrorFilter.Result;
import sonia.scm.repository.api.Pkcs12ClientCertificateCredential;
import sonia.scm.repository.api.UsernamePasswordCredential;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import static java.lang.String.format;
import static java.util.Collections.unmodifiableMap;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.FAILED;
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.OK;
import static sonia.scm.repository.api.MirrorCommandResult.ResultType.REJECTED_UPDATES;
/**
* Implementation of the mirror command for git. This implementation makes use of a special
* "ref" called <code>mirror</code>. A synchronization works in principal in the following way:
* <ol>
* <li>The mirror reference is updated. This is done by calling the jgit equivalent of
* <pre>git fetch -pf <source url> "refs/heads/*:refs/mirror/heads/*" "refs/tags/*:refs/mirror/tags/*"</pre>
* </li>
* <li>These updates are then presented to the filter. Here single updates can be rejected.
* Such rejected updates have to be reverted in the mirror, too.
* </li>
* <li>Accepted ref updates are copied to the "normal" refs.</li>
* </ol>
*/
public class GitMirrorCommand extends AbstractGitCommand implements MirrorCommand {
public static final String MIRROR_REF_PREFIX = "refs/mirror/";
private static final Logger LOG = LoggerFactory.getLogger(GitMirrorCommand.class);
private final PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory;
private final MirrorHttpConnectionProvider mirrorHttpConnectionProvider;
private final GitChangesetConverterFactory converterFactory;
private final GitTagConverter gitTagConverter;
@Inject
GitMirrorCommand(GitContext context, PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory, MirrorHttpConnectionProvider mirrorHttpConnectionProvider, GitChangesetConverterFactory converterFactory, GitTagConverter gitTagConverter) {
super(context);
this.mirrorHttpConnectionProvider = mirrorHttpConnectionProvider;
this.postReceiveRepositoryHookEventFactory = postReceiveRepositoryHookEventFactory;
this.converterFactory = converterFactory;
this.gitTagConverter = gitTagConverter;
}
@Override
public MirrorCommandResult mirror(MirrorCommandRequest mirrorCommandRequest) {
return update(mirrorCommandRequest);
}
@Override
public MirrorCommandResult update(MirrorCommandRequest mirrorCommandRequest) {
try (Repository repository = context.open(); Git git = Git.wrap(repository)) {
return new Worker(mirrorCommandRequest, repository, git).update();
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "error during git fetch", e);
}
}
private class Worker {
private final MirrorCommandRequest mirrorCommandRequest;
private final List<String> mirrorLog = new ArrayList<>();
private final Stopwatch stopwatch;
private final Repository repository;
private final Git git;
private FetchResult fetchResult;
private GitFilterContext filterContext;
private MirrorFilter.Filter filter;
private ResultType result = OK;
private Worker(MirrorCommandRequest mirrorCommandRequest, Repository repository, Git git) {
this.mirrorCommandRequest = mirrorCommandRequest;
this.repository = repository;
this.git = git;
stopwatch = Stopwatch.createStarted();
}
MirrorCommandResult update() {
try {
return doUpdate();
} catch (GitAPIException e) {
result = FAILED;
mirrorLog.add("failed to synchronize: " + e.getMessage());
return new MirrorCommandResult(FAILED, mirrorLog, stopwatch.stop().elapsed());
}
}
private MirrorCommandResult doUpdate() throws GitAPIException {
fetchResult = createFetchCommand().call();
filterContext = new GitFilterContext();
filter = mirrorCommandRequest.getFilter().getFilter(filterContext);
if (fetchResult.getTrackingRefUpdates().isEmpty()) {
mirrorLog.add("No updates found");
} else {
handleBranches();
handleTags();
}
postReceiveRepositoryHookEventFactory.fireForFetch(git, fetchResult);
return new MirrorCommandResult(result, mirrorLog, stopwatch.stop().elapsed());
}
private void handleBranches() {
LoggerWithHeader logger = new LoggerWithHeader("Branches:");
doForEachRefStartingWith(MIRROR_REF_PREFIX + "heads", ref -> handleBranch(logger, ref));
}
private void handleBranch(LoggerWithHeader logger, TrackingRefUpdate ref) {
MirrorReferenceUpdateHandler refHandler = new MirrorReferenceUpdateHandler(logger, ref, "heads/", "branch");
refHandler.handleRef(ref1 -> refHandler.testFilterForBranch());
}
private void handleTags() {
LoggerWithHeader logger = new LoggerWithHeader("Tags:");
doForEachRefStartingWith(MIRROR_REF_PREFIX + "tags", ref -> handleTag(logger, ref));
}
private void handleTag(LoggerWithHeader logger, TrackingRefUpdate ref) {
MirrorReferenceUpdateHandler refHandler = new MirrorReferenceUpdateHandler(logger, ref, "tags/", "tag");
refHandler.handleRef(ref1 -> refHandler.testFilterForTag());
}
private class MirrorReferenceUpdateHandler {
private final LoggerWithHeader logger;
private final TrackingRefUpdate ref;
private final String refType;
private final String typeForLog;
public MirrorReferenceUpdateHandler(LoggerWithHeader logger, TrackingRefUpdate ref, String refType, String typeForLog) {
this.logger = logger;
this.ref = ref;
this.refType = refType;
this.typeForLog = typeForLog;
}
private void handleRef(Function<TrackingRefUpdate, Result> filter) {
Result filterResult = filter.apply(ref);
try {
String referenceName = ref.getLocalName().substring(MIRROR_REF_PREFIX.length() + refType.length());
if (filterResult.isAccepted()) {
handleAcceptedReference(referenceName);
} else {
handleRejectedRef(referenceName, filterResult);
}
} catch (Exception e) {
handleReferenceUpdateException(e);
}
}
private Result testFilterForBranch() {
try {
return filter.acceptBranch(filterContext.getBranchUpdate(ref.getLocalName()));
} catch (Exception e) {
return handleExceptionFromFilter(e);
}
}
private void handleReferenceUpdateException(Exception e) {
LOG.warn("got exception processing ref {} in repository {}", ref.getLocalName(), GitMirrorCommand.this.repository, e);
mirrorLog.add(format("got error processing reference %s: %s", ref.getLocalName(), e.getMessage()));
mirrorLog.add("mirror may be damaged");
}
private void handleRejectedRef(String referenceName, Result filterResult) throws IOException {
result = REJECTED_UPDATES;
LOG.trace("{} ref rejected in {}: {}", typeForLog, GitMirrorCommand.this.repository, ref.getLocalName());
if (ref.getResult() == NEW) {
deleteReference(ref.getLocalName());
} else {
updateReference(ref.getLocalName(), ref.getOldObjectId());
}
logger.logChange(ref, referenceName, filterResult.getRejectReason().orElse("rejected due to filter"));
}
private void handleAcceptedReference(String referenceName) throws IOException {
String targetRef = "refs/" + refType + referenceName;
if (isDeletedReference(ref)) {
LOG.trace("deleting {} ref in {}: {}", typeForLog, GitMirrorCommand.this.repository, targetRef);
deleteReference(targetRef);
logger.logChange(ref, referenceName, "deleted");
} else {
LOG.trace("updating {} ref in {}: {}", typeForLog, GitMirrorCommand.this.repository, targetRef);
updateReference(targetRef, ref.getNewObjectId());
logger.logChange(ref, referenceName, getUpdateType(ref));
}
}
private Result testFilterForTag() {
try {
return filter.acceptTag(filterContext.getTagUpdate(ref.getLocalName()));
} catch (Exception e) {
return handleExceptionFromFilter(e);
}
}
private Result handleExceptionFromFilter(Exception e) {
LOG.warn("got exception from filter for ref {} in repository {}", ref.getLocalName(), GitMirrorCommand.this.repository, e);
mirrorLog.add("! got error checking filter for update: " + e.getMessage());
return Result.reject("exception in filter");
}
private void deleteReference(String targetRef) throws IOException {
RefUpdate deleteUpdate = repository.getRefDatabase().newUpdate(targetRef, true);
deleteUpdate.setForceUpdate(true);
deleteUpdate.delete();
}
private boolean isDeletedReference(TrackingRefUpdate ref) {
return ref.asReceiveCommand().getType() == ReceiveCommand.Type.DELETE;
}
private void updateReference(String reference, ObjectId objectId) throws IOException {
LOG.trace("updating ref in {}: {} -> {}", GitMirrorCommand.this.repository, reference, objectId);
RefUpdate refUpdate = repository.getRefDatabase().newUpdate(reference, true);
refUpdate.setNewObjectId(objectId);
refUpdate.forceUpdate();
}
private String getUpdateType(TrackingRefUpdate trackingRefUpdate) {
return trackingRefUpdate.getResult().name().toLowerCase(Locale.ENGLISH);
}
}
private class LoggerWithHeader {
private final String header;
private boolean headerWritten = false;
private LoggerWithHeader(String header) {
this.header = header;
}
void logChange(TrackingRefUpdate ref, String branchName, String type) {
logLine(
format("- %s..%s %s (%s)",
ref.getOldObjectId().abbreviate(9).name(),
ref.getNewObjectId().abbreviate(9).name(),
branchName,
type
));
}
void logLine(String line) {
if (!headerWritten) {
headerWritten = true;
mirrorLog.add(header);
}
mirrorLog.add(line);
}
}
private void doForEachRefStartingWith(String prefix, RefUpdateConsumer refUpdateConsumer) {
fetchResult.getTrackingRefUpdates()
.stream()
.filter(ref -> ref.getLocalName().startsWith(prefix))
.forEach(ref -> {
try {
refUpdateConsumer.accept(ref);
} catch (IOException e) {
throw new InternalRepositoryException(GitMirrorCommand.this.repository, "error updating mirror references", e);
}
});
}
private FetchCommand createFetchCommand() {
FetchCommand fetchCommand = Git.wrap(repository).fetch()
.setRefSpecs("refs/heads/*:" + MIRROR_REF_PREFIX + "heads/*", "refs/tags/*:" + MIRROR_REF_PREFIX + "tags/*")
.setForceUpdate(true)
.setRemoveDeletedRefs(true)
.setRemote(mirrorCommandRequest.getSourceUrl());
mirrorCommandRequest.getCredential(Pkcs12ClientCertificateCredential.class)
.ifPresent(c -> fetchCommand.setTransportConfigCallback(transport -> {
if (transport instanceof TransportHttp) {
TransportHttp transportHttp = (TransportHttp) transport;
transportHttp.setHttpConnectionFactory(mirrorHttpConnectionProvider.createHttpConnectionFactory(c, mirrorLog));
}
}));
mirrorCommandRequest.getCredential(UsernamePasswordCredential.class)
.ifPresent(c -> fetchCommand
.setCredentialsProvider(
new UsernamePasswordCredentialsProvider(
Strings.nullToEmpty(c.username()),
Strings.nullToEmpty(new String(c.password()))
))
);
return fetchCommand;
}
private class GitFilterContext implements MirrorFilter.FilterContext {
private final Map<String, MirrorFilter.BranchUpdate> branchUpdates;
private final Map<String, MirrorFilter.TagUpdate> tagUpdates;
public GitFilterContext() {
Map<String, MirrorFilter.BranchUpdate> extractedBranchUpdates = new HashMap<>();
Map<String, MirrorFilter.TagUpdate> extractedTagUpdates = new HashMap<>();
fetchResult.getTrackingRefUpdates().forEach(refUpdate -> {
if (refUpdate.getLocalName().startsWith(MIRROR_REF_PREFIX + "heads")) {
extractedBranchUpdates.put(refUpdate.getLocalName(), new GitBranchUpdate(refUpdate));
}
if (refUpdate.getLocalName().startsWith(MIRROR_REF_PREFIX + "tags")) {
extractedTagUpdates.put(refUpdate.getLocalName(), new GitTagUpdate(refUpdate));
}
});
this.branchUpdates = unmodifiableMap(extractedBranchUpdates);
this.tagUpdates = unmodifiableMap(extractedTagUpdates);
}
@Override
public Collection<MirrorFilter.BranchUpdate> getBranchUpdates() {
return branchUpdates.values();
}
@Override
public Collection<MirrorFilter.TagUpdate> getTagUpdates() {
return tagUpdates.values();
}
MirrorFilter.BranchUpdate getBranchUpdate(String ref) {
return branchUpdates.get(ref);
}
MirrorFilter.TagUpdate getTagUpdate(String ref) {
return tagUpdates.get(ref);
}
}
private class GitBranchUpdate implements MirrorFilter.BranchUpdate {
private final TrackingRefUpdate refUpdate;
private final String branchName;
private Changeset changeset;
public GitBranchUpdate(TrackingRefUpdate refUpdate) {
this.refUpdate = refUpdate;
this.branchName = refUpdate.getLocalName().substring(MIRROR_REF_PREFIX.length() + "heads/".length());
}
@Override
public String getBranchName() {
return branchName;
}
@Override
public Optional<Changeset> getChangeset() {
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.DELETE)) {
return empty();
}
if (changeset == null) {
changeset = computeChangeset();
}
return of(changeset);
}
@Override
public Optional<String> getNewRevision() {
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.DELETE)) {
return empty();
}
return of(refUpdate.getNewObjectId().name());
}
@Override
public Optional<String> getOldRevision() {
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.CREATE)) {
return empty();
}
return of(refUpdate.getOldObjectId().name());
}
@Override
public Optional<MirrorFilter.UpdateType> getUpdateType() {
return getUpdateTypeFor(refUpdate.asReceiveCommand());
}
@Override
public boolean isForcedUpdate() {
return refUpdate.getResult() == RefUpdate.Result.FORCED;
}
private Changeset computeChangeset() {
try (RevWalk revWalk = new RevWalk(repository); GitChangesetConverter gitChangesetConverter = converter(revWalk)) {
try {
RevCommit revCommit = revWalk.parseCommit(refUpdate.getNewObjectId());
return gitChangesetConverter.createChangeset(revCommit, refUpdate.getLocalName());
} catch (Exception e) {
throw new InternalRepositoryException(context.getRepository(), "got exception while validating branch", e);
}
}
}
private GitChangesetConverter converter(RevWalk revWalk) {
return converterFactory.builder(repository)
.withRevWalk(revWalk)
.withAdditionalPublicKeys(mirrorCommandRequest.getPublicKeys())
.create();
}
}
private class GitTagUpdate implements MirrorFilter.TagUpdate {
private final TrackingRefUpdate refUpdate;
private final String tagName;
private Tag tag;
public GitTagUpdate(TrackingRefUpdate refUpdate) {
this.refUpdate = refUpdate;
this.tagName = refUpdate.getLocalName().substring(MIRROR_REF_PREFIX.length() + "tags/".length());
}
@Override
public String getTagName() {
return tagName;
}
@Override
public Optional<Tag> getTag() {
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.DELETE)) {
return empty();
}
if (tag == null) {
tag = computeTag();
}
return of(tag);
}
@Override
public Optional<String> getNewRevision() {
return getTag().map(Tag::getRevision);
}
@Override
public Optional<String> getOldRevision() {
if (isOfTypeOrEmpty(getUpdateType(), MirrorFilter.UpdateType.CREATE)) {
return empty();
}
return of(refUpdate.getOldObjectId().name());
}
@Override
public Optional<MirrorFilter.UpdateType> getUpdateType() {
return getUpdateTypeFor(refUpdate.asReceiveCommand());
}
private Tag computeTag() {
try (RevWalk revWalk = new RevWalk(repository)) {
try {
RevObject revObject = revWalk.parseAny(refUpdate.getNewObjectId());
if (revObject.getType() == Constants.OBJ_TAG) {
RevTag revTag = revWalk.parseTag(revObject.getId());
return gitTagConverter.buildTag(revTag, revWalk);
} else if (revObject.getType() == Constants.OBJ_COMMIT) {
Ref ref = repository.getRefDatabase().findRef(refUpdate.getLocalName());
Tag t = gitTagConverter.buildTag(repository, revWalk, ref);
return new Tag(tagName, t.getRevision(), t.getDate().orElse(null), t.getDeletable());
} else {
throw new InternalRepositoryException(context.getRepository(), "invalid object type for tag");
}
} catch (Exception e) {
throw new InternalRepositoryException(context.getRepository(), "got exception while validating tag", e);
}
}
}
}
private boolean isOfTypeOrEmpty(Optional<MirrorFilter.UpdateType> updateType, MirrorFilter.UpdateType type) {
return !updateType.isPresent() || updateType.get() == type;
}
private Optional<MirrorFilter.UpdateType> getUpdateTypeFor(ReceiveCommand receiveCommand) {
switch (receiveCommand.getType()) {
case UPDATE:
case UPDATE_NONFASTFORWARD:
return of(MirrorFilter.UpdateType.UPDATE);
case CREATE:
return of(MirrorFilter.UpdateType.CREATE);
case DELETE:
return of(MirrorFilter.UpdateType.DELETE);
default:
return empty();
}
}
}
private interface RefUpdateConsumer {
void accept(TrackingRefUpdate refUpdate) throws IOException;
}
}

View File

@@ -32,26 +32,20 @@ import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.TagOpt;
import org.eclipse.jgit.transport.TrackingRefUpdate;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.ImportFailedException;
import sonia.scm.repository.api.PullResponse;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Sebastian Sdorra
@@ -59,19 +53,15 @@ import java.util.stream.Collectors;
public class GitPullCommand extends AbstractGitPushOrPullCommand
implements PullCommand {
private static final String REF_SPEC = "refs/heads/*:refs/heads/*";
private static final Logger LOG = LoggerFactory.getLogger(GitPullCommand.class);
private final ScmEventBus eventBus;
private final GitRepositoryHookEventFactory eventFactory;
private final PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory;
@Inject
public GitPullCommand(GitRepositoryHandler handler,
GitContext context,
ScmEventBus eventBus,
GitRepositoryHookEventFactory eventFactory) {
PostReceiveRepositoryHookEventFactory postReceiveRepositoryHookEventFactory) {
super(handler, context);
this.eventBus = eventBus;
this.eventFactory = eventFactory;
this.postReceiveRepositoryHookEventFactory = postReceiveRepositoryHookEventFactory;
}
@Override
@@ -158,8 +148,8 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
org.eclipse.jgit.lib.Repository source = null;
try {
source = Git.open(sourceDirectory).getRepository();
try (Git git = Git.open(sourceDirectory)) {
source = git.getRepository();
response = new PullResponse(push(source, getRemoteUrl(targetDirectory)));
} finally {
GitUtil.close(source);
@@ -177,16 +167,14 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
FetchResult result;
try {
//J-
result = git.fetch()
result = GitUtil.createFetchCommandWithBranchAndTagUpdate(git)
.setCredentialsProvider(
new UsernamePasswordCredentialsProvider(
Strings.nullToEmpty(request.getUsername()),
Strings.nullToEmpty(request.getUsername()),
Strings.nullToEmpty(request.getPassword())
)
)
.setRefSpecs(new RefSpec(REF_SPEC))
.setRemote(request.getRemoteUrl().toExternalForm())
.setTagOpt(TagOpt.FETCH_TAGS)
.call();
//J+
@@ -206,31 +194,6 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
}
private void firePostReceiveRepositoryHookEvent(Git git, FetchResult result) {
try {
List<String> branches = getBranchesFromFetchResult(result);
List<Tag> tags = getTagsFromFetchResult(result);
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git);
eventBus.post(eventFactory.createEvent(context, branches, tags, changesetResolver));
} catch (IOException e) {
throw new ImportFailedException(
ContextEntry.ContextBuilder.entity(context.getRepository()).build(),
"Could not fire post receive repository hook event after unbundle",
e
);
}
}
private List<Tag> getTagsFromFetchResult(FetchResult result) {
return result.getAdvertisedRefs().stream()
.filter(r -> r.getName().startsWith("refs/tags/"))
.map(r -> new Tag(r.getName().substring("refs/tags/".length()), r.getObjectId().getName()))
.collect(Collectors.toList());
}
private List<String> getBranchesFromFetchResult(FetchResult result) {
return result.getAdvertisedRefs().stream()
.filter(r -> r.getName().startsWith("refs/heads/"))
.map(r -> r.getLeaf().getName().substring("refs/heads/".length()))
.collect(Collectors.toList());
postReceiveRepositoryHookEventFactory.fireForFetch(git, result);
}
}

View File

@@ -56,7 +56,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
Command.MERGE,
Command.MODIFY,
Command.BUNDLE,
Command.UNBUNDLE
Command.UNBUNDLE,
Command.MIRROR
);
protected static final Set<Feature> FEATURES = EnumSet.of(Feature.INCOMING_REVISION);
@@ -171,6 +172,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
return commandInjector.getInstance(GitUnbundleCommand.class);
}
@Override
public MirrorCommand getMirrorCommand() {
return commandInjector.getInstance(GitMirrorCommand.class);
}
@Override
public Set<Command> getSupportedCommands() {
return COMMANDS;

View File

@@ -0,0 +1,88 @@
/*
* 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.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.Tag;
import sonia.scm.security.GPG;
import javax.inject.Inject;
import java.io.IOException;
class GitTagConverter {
private static final Logger LOG = LoggerFactory.getLogger(GitTagConverter.class);
private final GPG gpg;
@Inject
GitTagConverter(GPG gpg) {
this.gpg = gpg;
}
public Tag buildTag(RevTag revTag, RevWalk revWalk) {
Tag tag = null;
try {
RevCommit revCommit = revWalk.parseCommit(revTag.getObject().getId());
tag = new Tag(revTag.getTagName(), revCommit.getId().name(), revTag.getTaggerIdent().getWhen().getTime());
GitUtil.getTagSignature(revTag, gpg, revWalk).ifPresent(tag::addSignature);
} catch (IOException ex) {
LOG.error("could not get commit for tag", ex);
}
return tag;
}
public Tag buildTag(Repository repository, RevWalk revWalk, Ref ref) {
Tag tag = null;
try {
RevCommit revCommit = GitUtil.getCommit(repository, revWalk, ref);
if (revCommit != null) {
String name = GitUtil.getTagName(ref);
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) {
LOG.error("could not get commit for tag", ex);
}
return tag;
}
}

View File

@@ -26,28 +26,19 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
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;
import static java.util.stream.Collectors.toList;
//~--- JDK imports ------------------------------------------------------------
/**
@@ -55,7 +46,7 @@ import java.util.List;
*/
public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
private final GPG gpg;
private final GitTagConverter gitTagConverter;
/**
* Constructs ...
@@ -63,108 +54,23 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
* @param context
*/
@Inject
public GitTagsCommand(GitContext context, GPG gpp) {
public GitTagsCommand(GitContext context, GitTagConverter gitTagConverter) {
super(context);
this.gpg = gpp;
this.gitTagConverter = gitTagConverter;
}
//~--- get methods ----------------------------------------------------------
@Override
public List<Tag> getTags() throws IOException {
List<Tag> tags;
RevWalk revWalk = null;
try (Git git = new Git(open())) {
revWalk = new RevWalk(git.getRepository());
try (Git git = new Git(open()); RevWalk revWalk = new RevWalk(git.getRepository())) {
List<Ref> tagList = git.tagList().call();
tags = Lists.transform(tagList,
new TransformFunction(git.getRepository(), revWalk, gpg));
return tagList.stream()
.map(ref -> gitTagConverter.buildTag(git.getRepository(), revWalk, ref))
.collect(toList());
} catch (GitAPIException ex) {
throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
} finally {
GitUtil.release(revWalk);
}
return tags;
}
//~--- inner classes --------------------------------------------------------
/**
* Class description
*
* @author Enter your name here...
* @version Enter version here..., 12/07/06
*/
private static class TransformFunction implements Function<Ref, Tag> {
/**
* the logger for TransformFuntion
*/
private static final Logger logger =
LoggerFactory.getLogger(TransformFunction.class);
//~--- constructors -------------------------------------------------------
/**
* Constructs ...
* @param repository
* @param revWalk
*/
public TransformFunction(Repository repository,
RevWalk revWalk,
GPG gpg) {
this.repository = repository;
this.revWalk = revWalk;
this.gpg = gpg;
}
//~--- methods ------------------------------------------------------------
/**
* Method description
*
* @param ref
* @return
*/
@Override
public Tag apply(Ref ref) {
Tag tag = null;
try {
RevCommit revCommit = GitUtil.getCommit(repository, revWalk, ref);
if (revCommit != null) {
String name = GitUtil.getTagName(ref);
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);
}
return tag;
}
//~--- fields -------------------------------------------------------------
/**
* Field description
*/
private final org.eclipse.jgit.lib.Repository repository;
/**
* Field description
*/
private final RevWalk revWalk;
private final GPG gpg;
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.eclipse.jgit.transport.http.HttpConnectionFactory2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.api.Pkcs12ClientCertificateCredential;
import sonia.scm.web.ScmHttpConnectionFactory;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManager;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.List;
class MirrorHttpConnectionProvider {
private static final Logger LOG = LoggerFactory.getLogger(MirrorHttpConnectionProvider.class);
private final Provider<TrustManager> trustManagerProvider;
@Inject
public MirrorHttpConnectionProvider(Provider<TrustManager> trustManagerProvider) {
this.trustManagerProvider = trustManagerProvider;
}
public HttpConnectionFactory2 createHttpConnectionFactory(Pkcs12ClientCertificateCredential credential, List<String> log) {
return new ScmHttpConnectionFactory(trustManagerProvider, createKeyManagers(credential, log));
}
private KeyManager[] createKeyManagers(Pkcs12ClientCertificateCredential credential, List<String> log) {
try {
KeyStore pkcs12 = KeyStore.getInstance("PKCS12");
pkcs12.load(new ByteArrayInputStream(credential.getCertificate()), credential.getPassword());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(pkcs12, credential.getPassword());
log.add("added pkcs12 certificate");
return keyManagerFactory.getKeyManagers();
} catch (IOException | GeneralSecurityException e) {
LOG.info("could not create key store from pkcs12 credential", e);
log.add("failed to add pkcs12 certificate: " + e.getMessage());
}
return new KeyManager[0];
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.eclipse.jgit.api.Git;
import org.eclipse.jgit.transport.FetchResult;
import sonia.scm.ContextEntry;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.Tag;
import sonia.scm.repository.WrappedRepositoryHookEvent;
import sonia.scm.repository.api.ImportFailedException;
import javax.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
class PostReceiveRepositoryHookEventFactory {
private final ScmEventBus eventBus;
private final GitRepositoryHookEventFactory eventFactory;
private final GitContext context;
@Inject
PostReceiveRepositoryHookEventFactory(ScmEventBus eventBus, GitRepositoryHookEventFactory eventFactory, GitContext context) {
this.eventBus = eventBus;
this.eventFactory = eventFactory;
this.context = context;
}
void fireForFetch(Git git, FetchResult result) {
PostReceiveRepositoryHookEvent event;
try {
List<String> branches = getBranchesFromFetchResult(result);
List<Tag> tags = getTagsFromFetchResult(result);
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git);
event = new PostReceiveRepositoryHookEvent(WrappedRepositoryHookEvent.wrap(eventFactory.createEvent(context, branches, tags, changesetResolver)));
} catch (IOException e) {
throw new ImportFailedException(
ContextEntry.ContextBuilder.entity(context.getRepository()).build(),
"Could not fire post receive repository hook event after fetch",
e
);
}
eventBus.post(event);
}
private List<Tag> getTagsFromFetchResult(FetchResult result) {
return result.getAdvertisedRefs().stream()
.filter(r -> r.getName().startsWith("refs/tags/"))
.map(r -> new Tag(r.getName().substring("refs/tags/".length()), r.getObjectId().getName()))
.collect(Collectors.toList());
}
private List<String> getBranchesFromFetchResult(FetchResult result) {
return result.getAdvertisedRefs().stream()
.filter(r -> r.getName().startsWith("refs/heads/"))
.map(r -> r.getLeaf().getName().substring("refs/heads/".length()))
.collect(Collectors.toList());
}
}

View File

@@ -38,16 +38,13 @@ import sonia.scm.repository.spi.SimpleGitWorkingCopyFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
*
* @author Sebastian Sdorra
*/
@Extension
public class GitServletModule extends ServletModule
{
public class GitServletModule extends ServletModule {
@Override
protected void configureServlets()
{
protected void configureServlets() {
bind(GitRepositoryViewer.class);
bind(GitRepositoryResolver.class);
bind(GitReceivePackFactory.class);

View File

@@ -30,6 +30,7 @@ import com.google.inject.Singleton;
import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.lfs.lib.Constants;
import org.slf4j.Logger;
import sonia.scm.protocolcommand.git.ScmUploadPackFactoryForHttpServletRequest;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryRequestListenerUtil;
import sonia.scm.repository.spi.ScmProviderHttpServlet;
@@ -71,6 +72,7 @@ public class ScmGitServlet extends GitServlet implements ScmProviderHttpServlet
@Inject
public ScmGitServlet(GitRepositoryResolver repositoryResolver,
GitReceivePackFactory receivePackFactory,
ScmUploadPackFactoryForHttpServletRequest scmUploadPackFactory,
GitRepositoryViewer repositoryViewer,
RepositoryRequestListenerUtil repositoryRequestListenerUtil,
LfsServletFactory lfsServletFactory)
@@ -81,6 +83,7 @@ public class ScmGitServlet extends GitServlet implements ScmProviderHttpServlet
setRepositoryResolver(repositoryResolver);
setReceivePackFactory(receivePackFactory);
setUploadPackFactory(scmUploadPackFactory);
}
//~--- methods --------------------------------------------------------------

View File

@@ -0,0 +1,98 @@
/*
* 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.web;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.transport.http.HttpConnection;
import org.eclipse.jgit.transport.http.JDKHttpConnection;
import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory;
import org.eclipse.jgit.transport.http.NoCheckX509TrustManager;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;
import java.security.GeneralSecurityException;
import java.text.MessageFormat;
public class ScmHttpConnectionFactory extends JDKHttpConnectionFactory {
private final Provider<TrustManager> trustManagerProvider;
private final KeyManager[] keyManagers;
@Inject
public ScmHttpConnectionFactory(Provider<TrustManager> trustManagerProvider) {
this(trustManagerProvider, null);
}
public ScmHttpConnectionFactory(Provider<TrustManager> trustManagerProvider, KeyManager[] keyManagers) {
this.trustManagerProvider = trustManagerProvider;
this.keyManagers = keyManagers;
}
@Override
public GitSession newSession() {
return new ScmConnectionSession(trustManagerProvider.get(), keyManagers);
}
private static class ScmConnectionSession implements GitSession {
private final TrustManager trustManager;
private final KeyManager[] keyManagers;
private ScmConnectionSession(TrustManager trustManager, KeyManager[] keyManagers) {
this.trustManager = trustManager;
this.keyManagers = keyManagers;
}
@Override
@SuppressWarnings("java:S5527")
public JDKHttpConnection configure(HttpConnection connection,
boolean sslVerify) throws GeneralSecurityException {
if (!(connection instanceof JDKHttpConnection)) {
throw new IllegalArgumentException(MessageFormat.format(
JGitText.get().httpWrongConnectionType,
JDKHttpConnection.class.getName(),
connection.getClass().getName()));
}
JDKHttpConnection conn = (JDKHttpConnection) connection;
String scheme = conn.getURL().getProtocol();
if ("https".equals(scheme) && sslVerify) { //$NON-NLS-1$
// sslVerify == true: use the JDK defaults
conn.configure(keyManagers, new TrustManager[]{trustManager}, null);
} else if ("https".equals(scheme)) {
conn.configure(keyManagers, new TrustManager[]{new NoCheckX509TrustManager()}, null);
conn.setHostnameVerifier((name, value) -> true);
}
return conn;
}
@Override
public void close() {
// Nothing
}
}
}