diff --git a/gradle/changelog/fast_git_modifications.yaml b/gradle/changelog/fast_git_modifications.yaml new file mode 100644 index 0000000000..81e9d48e24 --- /dev/null +++ b/gradle/changelog/fast_git_modifications.yaml @@ -0,0 +1,2 @@ +- type: added + description: Performance improvements for git modifications diff --git a/gradle/changelog/jgit7.yaml b/gradle/changelog/jgit7.yaml new file mode 100644 index 0000000000..114ac1a9b5 --- /dev/null +++ b/gradle/changelog/jgit7.yaml @@ -0,0 +1,2 @@ +- type: changed + description: Upgrade JGit to 7.1.0.202411261347-r diff --git a/scm-plugins/scm-git-plugin/build.gradle b/scm-plugins/scm-git-plugin/build.gradle index f79349dca2..fc66dce344 100644 --- a/scm-plugins/scm-git-plugin/build.gradle +++ b/scm-plugins/scm-git-plugin/build.gradle @@ -18,7 +18,7 @@ plugins { id 'org.scm-manager.smp' version '0.17.0' } -def jgitVersion = '6.7.0.202309050840-r-scm1-jakarta' +def jgitVersion = '7.1.0.202411261347-r-scm1' dependencies { // required by scm-it diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHeadModifier.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHeadModifier.java index 0eb4620739..34f32d01d8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHeadModifier.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHeadModifier.java @@ -48,11 +48,10 @@ public class GitHeadModifier { * repositories head points already to the given branch. * * @param repository repository to modify - * @param newHead branch which should be the new head of the repository - * + * @param newHead branch which should be the new head of the repository * @return {@code true} if the head has changed */ - public boolean ensure(Repository repository, String newHead) { + public boolean ensure(Repository repository, String newHead) { try (org.eclipse.jgit.lib.Repository gitRepository = open(repository)) { String currentHead = resolve(gitRepository); if (!Objects.equals(currentHead, newHead)) { @@ -65,8 +64,8 @@ public class GitHeadModifier { } private String resolve(org.eclipse.jgit.lib.Repository gitRepository) throws IOException { - Ref ref = gitRepository.getRefDatabase().getRef(Constants.HEAD); - if ( ref.isSymbolic() ) { + Ref ref = gitRepository.getRefDatabase().findRef(Constants.HEAD); + if (ref.isSymbolic()) { ref = ref.getTarget(); } return GitUtil.getBranch(ref); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index f9c8b6f139..f2087ac5fe 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -239,9 +239,7 @@ public final class GitUtil { String branchName) throws IOException { Ref ref = null; - if (!branchName.startsWith(REF_HEAD)) { - branchName = PREFIX_HEADS.concat(branchName); - } + branchName = getRevString(branchName); checkBranchName(repo, branchName); @@ -258,6 +256,13 @@ public final class GitUtil { return ref; } + public static String getRevString(String branchName) { + if (!branchName.startsWith(REF_HEAD)) { + return PREFIX_HEADS.concat(branchName); + } + return branchName; + } + /** * @since 2.5.0 */ diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java index c3572f99a0..fda764f799 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java @@ -18,16 +18,18 @@ package sonia.scm.repository; import jakarta.inject.Inject; import org.eclipse.jgit.api.errors.CanceledException; -import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; +import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.GpgSignature; -import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Signer; import org.eclipse.jgit.transport.CredentialsProvider; import sonia.scm.security.GPG; -import java.io.UnsupportedEncodingException; +import java.io.IOException; -public class ScmGpgSigner extends GpgSigner { +public class ScmGpgSigner implements Signer { private final GPG gpg; @@ -37,17 +39,13 @@ public class ScmGpgSigner extends GpgSigner { } @Override - public void sign(CommitBuilder commitBuilder, String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { - try { - final byte[] signature = this.gpg.getPrivateKey().sign(commitBuilder.build()); - commitBuilder.setGpgSignature(new GpgSignature(signature)); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } + public GpgSignature sign(Repository repository, GpgConfig gpgConfig, byte[] bytes, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) throws CanceledException, IOException, UnsupportedSigningFormatException { + final byte[] signature = this.gpg.getPrivateKey().sign(bytes); + return new GpgSignature(signature); } @Override - public boolean canLocateSigningKey(String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { + public boolean canLocateSigningKey(Repository repository, GpgConfig gpgConfig, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) throws CanceledException { return true; } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java index 901b4fee76..3e485436f7 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java @@ -19,7 +19,8 @@ package sonia.scm.repository; import jakarta.inject.Inject; import jakarta.servlet.ServletContextEvent; import jakarta.servlet.ServletContextListener; -import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.GpgConfig; +import org.eclipse.jgit.lib.Signers; import sonia.scm.plugin.Extension; @Extension @@ -34,7 +35,7 @@ public class ScmGpgSignerInitializer implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) { - GpgSigner.setDefault(scmGpgSigner); + Signers.set(GpgConfig.GpgFormat.OPENPGP, scmGpgSigner); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index 02d8d24e25..2f6d68a794 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -78,7 +78,7 @@ class AbstractGitCommand { this.context = context; } - Repository open() throws IOException { + Repository open() { return context.open(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/CommitHelper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/CommitHelper.java new file mode 100644 index 0000000000..68139509be --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/CommitHelper.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2020 - present Cloudogu GmbH + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package sonia.scm.repository.spi; + +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgConfig; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Signer; +import org.eclipse.jgit.lib.Signers; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.CredentialsProvider; +import sonia.scm.ConcurrentModificationException; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Person; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.user.User; + +import java.io.IOException; +import java.util.List; + +import static java.util.Collections.emptyList; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +@Slf4j +class CommitHelper { + + private final Repository repository; + private final GitContext context; + private final RepositoryManager repositoryManager; + private final GitRepositoryHookEventFactory eventFactory; + + CommitHelper(GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) { + this.repository = context.open(); + this.context = context; + this.repositoryManager = repositoryManager; + this.eventFactory = eventFactory; + } + + ObjectId createCommit(ObjectId treeId, + Person author, + Person committer, + String message, + boolean sign, + ObjectId... parentCommitIds) throws IOException, CanceledException, UnsupportedSigningFormatException { + log.trace("create commit for tree {} and parent ids {} in repository {}", treeId, parentCommitIds, context.getRepository()); + try (ObjectInserter inserter = repository.newObjectInserter()) { + CommitBuilder commitBuilder = new CommitBuilder(); + commitBuilder.setTreeId(treeId); + commitBuilder.setParentIds(parentCommitIds); + commitBuilder.setAuthor(createPersonIdent(author)); + commitBuilder.setCommitter(createPersonIdent(committer)); + commitBuilder.setMessage(message); + if (sign) { + sign(commitBuilder, createPersonIdent(committer)); + } + ObjectId commitId = inserter.insert(commitBuilder); + inserter.flush(); + log.trace("created commit with id {}", commitId); + return commitId; + } + } + + private PersonIdent createPersonIdent(Person person) { + if (person == null) { + User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class); + return new PersonIdent(currentUser.getDisplayName(), currentUser.getMail()); + } + return new PersonIdent(person.getName(), person.getMail()); + } + + private void sign(CommitBuilder commit, PersonIdent committer) + throws CanceledException, IOException, UnsupportedSigningFormatException { + log.trace("sign commit"); + GpgConfig gpgConfig = new GpgConfig(repository.getConfig()); + Signer signer = Signers.get(gpgConfig.getKeyFormat()); + signer.signObject(repository, gpgConfig, commit, committer, "SCM-MANAGER-DEFAULT-KEY", CredentialsProvider.getDefault()); + } + + void updateBranch(String branchName, ObjectId newCommitId, ObjectId expectedOldObjectId) { + log.trace("update branch {} with new commit id {} in repository {}", branchName, newCommitId, context.getRepository()); + try { + RevCommit newCommit = findNewCommit(newCommitId); + firePreCommitHook(branchName, newCommit); + doUpdate(branchName, newCommitId, expectedOldObjectId); + firePostCommitHook(branchName, newCommit); + } catch (IOException e) { + throw new InternalRepositoryException(context.getRepository(), "could not update branch " + branchName, e); + } + } + + private RevCommit findNewCommit(ObjectId newCommitId) throws IOException { + RevCommit newCommit; + try (RevWalk revWalk = new RevWalk(repository)) { + newCommit = revWalk.parseCommit(newCommitId); + } + return newCommit; + } + + private void firePreCommitHook(String branchName, RevCommit newCommit) { + repositoryManager.fireHookEvent( + eventFactory.createPreReceiveEvent( + context, + List.of(branchName), + emptyList(), + () -> List.of(newCommit) + ) + ); + } + + private void doUpdate(String branchName, ObjectId newCommitId, ObjectId expectedOldObjectId) throws IOException { + RefUpdate refUpdate = repository.updateRef(GitUtil.getRevString(branchName)); + if (newCommitId == null) { + refUpdate.setExpectedOldObjectId(ObjectId.zeroId()); + } else { + refUpdate.setExpectedOldObjectId(expectedOldObjectId); + } + refUpdate.setNewObjectId(newCommitId); + refUpdate.setForceUpdate(false); + RefUpdate.Result result = refUpdate.update(); + + if (isSuccessfulUpdate(expectedOldObjectId, result)) { + throw new ConcurrentModificationException(entity("branch", branchName).in(context.getRepository()).build()); + } + } + + private void firePostCommitHook(String branchName, RevCommit newCommit) { + repositoryManager.fireHookEvent( + eventFactory.createPostReceiveEvent( + context, + List.of(branchName), + emptyList(), + () -> List.of(newCommit) + ) + ); + } + + private boolean isSuccessfulUpdate(ObjectId expectedOldObjectId, RefUpdate.Result result) { + return result != RefUpdate.Result.FAST_FORWARD && !(expectedOldObjectId == null && result == RefUpdate.Result.NEW); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java index 710eb35bf4..7c84fe521a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java @@ -97,7 +97,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman log.debug("got exception for invalid branch name {}", request.getNewBranch(), e); doThrow().violation("Invalid branch name", "name").when(true); return null; - } catch (GitAPIException | IOException ex) { + } catch (GitAPIException ex) { throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex); } } @@ -116,7 +116,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); } catch (CannotDeleteCurrentBranchException e) { throw new CannotDeleteDefaultBranchException(context.getRepository(), branchName); - } catch (GitAPIException | IOException ex) { + } catch (GitAPIException ex) { throw new InternalRepositoryException(entity(context.getRepository()), String.format("Could not delete branch: %s", branchName)); } } @@ -161,12 +161,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman @Override public HookChangesetProvider getChangesetProvider() { - Repository gitRepo; - try { - gitRepo = context.open(); - } catch (IOException e) { - throw new InternalRepositoryException(repository, "failed to open repository for post receive hook after internal change", e); - } + Repository gitRepo = context.open(); Collection receiveCommands = asList(createReceiveCommand()); return x -> { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java index 0679741e29..11cc1a032f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java @@ -53,7 +53,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo } @Override - public List getBranches() throws IOException { + public List getBranches() { Git git = createGit(); String defaultBranchName = determineDefaultBranchName(git); @@ -72,7 +72,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo } @VisibleForTesting - Git createGit() throws IOException { + Git createGit() { return new Git(open()); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java index d123206231..04e29ed1ea 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java @@ -23,6 +23,7 @@ import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryProvider; @@ -62,13 +63,17 @@ public class GitContext implements Closeable, RepositoryProvider } - public org.eclipse.jgit.lib.Repository open() throws IOException + public org.eclipse.jgit.lib.Repository open() { if (gitRepository == null) { logger.trace("open git repository {}", directory); - gitRepository = GitUtil.open(directory); + try { + gitRepository = GitUtil.open(directory); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "could not open git repository", e); + } } return gitRepository; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java index ba2396b01c..d6ea523239 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitFastForwardIfPossible.java @@ -16,38 +16,41 @@ package sonia.scm.repository.spi; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.MergeCommand; -import org.eclipse.jgit.api.MergeResult; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jgit.lib.ObjectId; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.MergeCommandResult; -import java.io.IOException; -import java.util.Collections; +@Slf4j +class GitFastForwardIfPossible { -class GitFastForwardIfPossible extends GitMergeStrategy { + private final MergeCommandRequest request; + private final MergeHelper mergeHelper; + private final GitMergeCommit fallbackMerge; + private final CommitHelper commitHelper; + private final Repository repository; - private GitMergeStrategy fallbackMerge; - - GitFastForwardIfPossible(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { - super(clone, request, context, repository); - fallbackMerge = new GitMergeCommit(clone, request, context, repository); + GitFastForwardIfPossible(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) { + this.request = request; + this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory); + this.fallbackMerge = new GitMergeCommit(request, context, repositoryManager, eventFactory); + this.commitHelper = new CommitHelper(context, repositoryManager, eventFactory); + this.repository = context.getRepository(); } - @Override - MergeCommandResult run() throws IOException { - MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); - if (fastForwardResult.getMergeStatus().isSuccessful()) { - push(); - return createSuccessResult(fastForwardResult.getNewHead().name()); + MergeCommandResult run() { + log.trace("try to fast forward branch {} onto {} in repository {}", request.getBranchToMerge(), request.getTargetBranch(), repository); + ObjectId sourceRevision = mergeHelper.getRevisionToMerge(); + ObjectId targetRevision = mergeHelper.getTargetRevision(); + + if (mergeHelper.isMergedInto(targetRevision, sourceRevision)) { + log.trace("fast forward branch {} onto {}", request.getBranchToMerge(), request.getTargetBranch()); + commitHelper.updateBranch(request.getTargetBranch(), sourceRevision, targetRevision); + return MergeCommandResult.success(targetRevision.name(), mergeHelper.getRevisionToMerge().name(), sourceRevision.name()); } else { + log.trace("fast forward is not possible, fallback to merge"); return fallbackMerge.run(); } } - - private MergeResult mergeWithFastForwardOnlyMode() throws IOException { - MergeCommand mergeCommand = getClone().merge(); - mergeCommand.setFastForward(MergeCommand.FastForwardMode.FF_ONLY); - return doMergeInClone(mergeCommand); - } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitImportHookContextProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitImportHookContextProvider.java index 34c5e1bf22..723a05db66 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitImportHookContextProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitImportHookContextProvider.java @@ -17,6 +17,7 @@ package sonia.scm.repository.spi; import com.google.common.collect.ImmutableSet; +import org.eclipse.jgit.revwalk.RevCommit; import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.Tag; import sonia.scm.repository.api.HookBranchProvider; @@ -27,17 +28,19 @@ import sonia.scm.repository.api.HookTagProvider; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.function.Supplier; class GitImportHookContextProvider extends HookContextProvider { private final GitChangesetConverter converter; private final List newTags; - private final GitLazyChangesetResolver changesetResolver; + private final Supplier> changesetResolver; private final List newBranches; GitImportHookContextProvider(GitChangesetConverter converter, List newBranches, List newTags, - GitLazyChangesetResolver changesetResolver) { + Supplier> changesetResolver) { this.converter = converter; this.newTags = newTags; this.changesetResolver = changesetResolver; @@ -81,7 +84,7 @@ class GitImportHookContextProvider extends HookContextProvider { @Override public HookChangesetProvider getChangesetProvider() { - GitConvertingChangesetIterable changesets = new GitConvertingChangesetIterable(changesetResolver.call(), converter); + GitConvertingChangesetIterable changesets = new GitConvertingChangesetIterable(changesetResolver.get(), converter); return r -> new HookChangesetResponse(changesets); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLazyChangesetResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLazyChangesetResolver.java index 7a911682c9..942c696b3c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLazyChangesetResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLazyChangesetResolver.java @@ -23,11 +23,11 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import java.io.IOException; -import java.util.concurrent.Callable; +import java.util.function.Supplier; import static sonia.scm.ContextEntry.ContextBuilder.entity; -class GitLazyChangesetResolver implements Callable> { +class GitLazyChangesetResolver implements Supplier> { private final Repository repository; private final Git git; @@ -37,7 +37,7 @@ class GitLazyChangesetResolver implements Callable> { } @Override - public Iterable call() { + public Iterable get() { try { return git.log().all().call(); } catch (IOException | GitAPIException e) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java index cd98f05450..169264a085 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java @@ -31,7 +31,6 @@ import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.InternalRepositoryException; import sonia.scm.util.IOUtil; import java.io.IOException; @@ -40,10 +39,9 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; -public class GitLogCommand extends AbstractGitCommand implements LogCommand -{ +public class GitLogCommand extends AbstractGitCommand implements LogCommand { + - private static final Logger logger = LoggerFactory.getLogger(GitLogCommand.class); public static final String REVISION = "Revision"; @@ -51,20 +49,16 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand @Inject - GitLogCommand(@Assisted GitContext context, GitChangesetConverterFactory converterFactory) - { + GitLogCommand(@Assisted GitContext context, GitChangesetConverterFactory converterFactory) { super(context); this.converterFactory = converterFactory; } - @Override @SuppressWarnings("java:S2093") - public Changeset getChangeset(String revision, LogCommandRequest request) - { - if (logger.isDebugEnabled()) - { + public Changeset getChangeset(String revision, LogCommandRequest request) { + if (logger.isDebugEnabled()) { logger.debug("fetch changeset {}", revision); } @@ -73,18 +67,15 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand GitChangesetConverter converter = null; RevWalk revWalk = null; - try - { + try { gr = open(); - if (!gr.getAllRefs().isEmpty()) - { + if (!gr.getAllRefs().isEmpty()) { revWalk = new RevWalk(gr); ObjectId id = GitUtil.getRevisionId(gr, revision); RevCommit commit = revWalk.parseCommit(id); - if (commit != null) - { + if (commit != null) { converter = converterFactory.create(gr, revWalk); if (isBranchRequested(request)) { @@ -98,23 +89,15 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand } else { changeset = converter.createChangeset(commit); } - } - else if (logger.isWarnEnabled()) - { + } else if (logger.isWarnEnabled()) { logger.warn("could not find revision {}", revision); } } - } - catch (IOException ex) - { + } catch (IOException ex) { logger.error("could not open repository: " + repository.getNamespaceAndName(), ex); - } - catch (NullPointerException e) - { + } catch (NullPointerException e) { throw notFound(entity(REVISION, revision).in(this.repository)); - } - finally - { + } finally { IOUtil.close(converter); GitUtil.release(revWalk); } @@ -138,14 +121,10 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand @Override @SuppressWarnings("java:S2093") public ChangesetPagingResult getChangesets(LogCommandRequest request) { - try { - if (Strings.isNullOrEmpty(request.getBranch())) { - request.setBranch(context.getConfig().getDefaultBranch()); - } - return new GitLogComputer(this.repository.getId(), open(), converterFactory).compute(request); - } catch (IOException e) { - throw new InternalRepositoryException(repository, "could not create change log", e); + if (Strings.isNullOrEmpty(request.getBranch())) { + request.setBranch(context.getConfig().getDefaultBranch()); } + return new GitLogComputer(this.repository.getId(), open(), converterFactory).compute(request); } public interface Factory { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java index 100edf8670..3b68a08004 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -32,6 +32,7 @@ import org.eclipse.jgit.treewalk.filter.PathFilter; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; import sonia.scm.repository.api.MergePreventReason; @@ -56,6 +57,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand private final GitWorkingCopyFactory workingCopyFactory; private final AttributeAnalyzer attributeAnalyzer; + private final RepositoryManager repositoryManager; + private final GitRepositoryHookEventFactory eventFactory; + private static final Set STRATEGIES = Set.of( MergeStrategy.MERGE_COMMIT, MergeStrategy.FAST_FORWARD_IF_POSSIBLE, @@ -64,14 +68,24 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand ); @Inject - GitMergeCommand(@Assisted GitContext context, GitRepositoryHandler handler, AttributeAnalyzer attributeAnalyzer) { - this(context, handler.getWorkingCopyFactory(), attributeAnalyzer); + GitMergeCommand(@Assisted GitContext context, + GitRepositoryHandler handler, + AttributeAnalyzer attributeAnalyzer, + RepositoryManager repositoryManager, + GitRepositoryHookEventFactory eventFactory) { + this(context, handler.getWorkingCopyFactory(), attributeAnalyzer, repositoryManager, eventFactory); } - GitMergeCommand(@Assisted GitContext context, GitWorkingCopyFactory workingCopyFactory, AttributeAnalyzer attributeAnalyzer) { + GitMergeCommand(@Assisted GitContext context, + GitWorkingCopyFactory workingCopyFactory, + AttributeAnalyzer attributeAnalyzer, + RepositoryManager repositoryManager, + GitRepositoryHookEventFactory eventFactory) { super(context); this.workingCopyFactory = workingCopyFactory; this.attributeAnalyzer = attributeAnalyzer; + this.repositoryManager = repositoryManager; + this.eventFactory = eventFactory; } @Override @@ -85,22 +99,14 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand } private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) { - switch (request.getMergeStrategy()) { - case SQUASH: - return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); - - case FAST_FORWARD_IF_POSSIBLE: - return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); - - case MERGE_COMMIT: - return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); - - case REBASE: - return inClone(clone -> new GitMergeRebase(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); - - default: - throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy()); - } + return switch (request.getMergeStrategy()) { + case SQUASH -> new GitMergeWithSquash(request, context, repositoryManager, eventFactory).run(); + case FAST_FORWARD_IF_POSSIBLE -> + new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory).run(); + case MERGE_COMMIT -> new GitMergeCommit(request, context, repositoryManager, eventFactory).run(); + case REBASE -> new GitMergeRebase(request, context, repositoryManager, eventFactory).run(); + default -> throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy()); + }; } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java index d06d4db962..98e930032f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommit.java @@ -16,39 +16,21 @@ package sonia.scm.repository.spi; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.MergeCommand; -import org.eclipse.jgit.api.MergeResult; -import org.eclipse.jgit.revwalk.RevCommit; -import sonia.scm.NoChangesMadeException; -import sonia.scm.repository.Repository; +import org.eclipse.jgit.lib.ObjectId; +import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.MergeCommandResult; -import java.io.IOException; -import java.util.Collections; -import java.util.Optional; +class GitMergeCommit { -import static sonia.scm.repository.spi.GitRevisionExtractor.extractRevisionFromRevCommit; + private final MergeCommandRequest request; + private final MergeHelper mergeHelper; -class GitMergeCommit extends GitMergeStrategy { - - GitMergeCommit(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { - super(clone, request, context, repository); + GitMergeCommit(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) { + this.request = request; + this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory); } - @Override - MergeCommandResult run() throws IOException { - MergeCommand mergeCommand = getClone().merge(); - mergeCommand.setFastForward(MergeCommand.FastForwardMode.NO_FF); - MergeResult result = doMergeInClone(mergeCommand); - - if (result.getMergeStatus().isSuccessful()) { - RevCommit revCommit = doCommit().orElseThrow(() -> new NoChangesMadeException(getRepository())); - push(); - return createSuccessResult(extractRevisionFromRevCommit(revCommit)); - } else { - return analyseFailure(result); - } + MergeCommandResult run() { + return mergeHelper.doRecursiveMerge(request, (sourceRevision, targetRevision) -> new ObjectId[]{targetRevision, sourceRevision}); } - } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeRebase.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeRebase.java index 12d6269b8d..72ba89bca6 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeRebase.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeRebase.java @@ -16,73 +16,108 @@ package sonia.scm.repository.spi; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.MergeCommand; -import org.eclipse.jgit.api.RebaseResult; -import org.eclipse.jgit.api.errors.GitAPIException; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; import org.eclipse.jgit.lib.ObjectId; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.merge.ResolveMerger; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.Repository; +import sonia.scm.repository.Person; +import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.MergeCommandResult; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; -import java.util.Optional; +import java.util.Iterator; +import java.util.List; -public class GitMergeRebase extends GitMergeStrategy { +import static java.util.Optional.ofNullable; +import static org.eclipse.jgit.merge.MergeStrategy.RESOLVE; - private static final Logger logger = LoggerFactory.getLogger(GitMergeRebase.class); +@Slf4j +class GitMergeRebase { private final MergeCommandRequest request; + private final GitContext context; + private final MergeHelper mergeHelper; + private final CommitHelper commitHelper; + private final GitFastForwardIfPossible fastForwardMerge; - GitMergeRebase(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { - super(clone, request, context, repository); + GitMergeRebase(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) { this.request = request; + this.context = context; + this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory); + this.commitHelper = new CommitHelper(context, repositoryManager, eventFactory); + this.fastForwardMerge = new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory); } - @Override - MergeCommandResult run() throws IOException { - RebaseResult result; - String branchToMerge = request.getBranchToMerge(); - String targetBranch = request.getTargetBranch(); - try { - checkOutBranch(branchToMerge); - result = - getClone() - .rebase() - .setUpstream(targetBranch) - .call(); - } catch (GitAPIException e) { - throw new InternalRepositoryException(getContext().getRepository(), "could not rebase branch " + branchToMerge + " onto " + targetBranch, e); + MergeCommandResult run() { + log.debug("rebase branch {} onto {}", request.getBranchToMerge(), request.getTargetBranch()); + + ObjectId sourceRevision = mergeHelper.getRevisionToMerge(); + ObjectId targetRevision = mergeHelper.getTargetRevision(); + if (mergeHelper.isMergedInto(targetRevision, sourceRevision)) { + log.trace("fast forward is possible; using fast forward merge"); + return fastForwardMerge.run(); } - if (result.getStatus().isSuccessful()) { - return fastForwardTargetBranch(branchToMerge, targetBranch, result); - } else { - logger.info("could not rebase branch {} into {} with rebase status '{}' due to ...", branchToMerge, targetBranch, result.getStatus()); - logger.info("... conflicts: {}", result.getConflicts()); - logger.info("... failing paths: {}", result.getFailingPaths()); - logger.info("... message: {}", result); - return MergeCommandResult.failure(branchToMerge, targetBranch, Optional.ofNullable(result.getConflicts()).orElse(Collections.singletonList("UNKNOWN"))); + try { + List commits = computeCommits(); + Collections.reverse(commits); + + for (RevCommit commit : commits) { + log.trace("rebase {} onto {}", commit, targetRevision); + ResolveMerger merger = (ResolveMerger) RESOLVE.newMerger(context.open(), true); // The recursive merger is always a RecursiveMerge + merger.setBase(commit.getParent(0)); + boolean mergeSucceeded = merger.merge(commit, targetRevision); + if (!mergeSucceeded) { + log.trace("could not merge {} into {}", commit, targetRevision); + return MergeCommandResult.failure(request.getBranchToMerge(), request.getTargetBranch(), ofNullable(merger.getUnmergedPaths()).orElse(Collections.singletonList("UNKNOWN"))); + } + ObjectId newTreeId = merger.getResultTreeId(); + log.trace("create commit for new tree {}", newTreeId); + + PersonIdent originalAuthor = commit.getAuthorIdent(); + targetRevision = commitHelper.createCommit( + newTreeId, + new Person(originalAuthor.getName(), originalAuthor.getEmailAddress()), + request.getAuthor(), + commit.getFullMessage(), + request.isSign(), + targetRevision + ); + log.trace("created {}", targetRevision); + } + log.trace("update branch {} to new revision {}", request.getTargetBranch(), targetRevision); + commitHelper.updateBranch(request.getTargetBranch(), targetRevision, mergeHelper.getTargetRevision()); + return MergeCommandResult.success(targetRevision.name(), mergeHelper.getRevisionToMerge().name(), targetRevision.name()); + } catch (IOException | CanceledException | UnsupportedSigningFormatException e) { + throw new InternalRepositoryException(context.getRepository(), "could not rebase branch " + request.getBranchToMerge() + " onto " + request.getTargetBranch(), e); } } - private MergeCommandResult fastForwardTargetBranch(String branchToMerge, String targetBranch, RebaseResult result) throws IOException { - try { - getClone().checkout().setName(targetBranch).call(); - ObjectId sourceRevision = resolveRevision(branchToMerge); - getClone() - .merge() - .setFastForward(MergeCommand.FastForwardMode.FF_ONLY) - .include(branchToMerge, sourceRevision) - .call(); - push(); - return createSuccessResult(sourceRevision.name()); - } catch (GitAPIException e) { - return MergeCommandResult.failure(branchToMerge, targetBranch, result.getConflicts()); - } + private List computeCommits() throws IOException { + List cherryPickList = new ArrayList<>(); + try (RevWalk revWalk = new RevWalk(context.open())) { + revWalk.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true); + revWalk.sort(RevSort.COMMIT_TIME_DESC, true); + revWalk.markUninteresting(revWalk.lookupCommit(mergeHelper.getTargetRevision())); + revWalk.markStart(revWalk.lookupCommit(mergeHelper.getRevisionToMerge())); + for (RevCommit commit : revWalk) { + if (commit.getParentCount() <= 1) { + log.trace("add {} to cherry pick list", commit); + cherryPickList.add(commit); + } else { + log.trace("skip {} because it has more than one parent", commit); + } + } + } + return cherryPickList; } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java deleted file mode 100644 index 2d073577f0..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2020 - present Cloudogu GmbH - * - * This program is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ - -package sonia.scm.repository.spi; - -import com.google.common.base.Strings; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.MergeCommand; -import org.eclipse.jgit.api.MergeResult; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.revwalk.RevCommit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.Person; -import sonia.scm.repository.api.MergeCommandResult; - -import java.io.IOException; -import java.text.MessageFormat; -import java.util.Optional; - -abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker { - - private static final Logger logger = LoggerFactory.getLogger(GitMergeStrategy.class); - - private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n", - "Merge of branch {0} into {1}", - "", - "Automatic merge by SCM-Manager."); - - private final String targetBranch; - private final ObjectId targetRevision; - private final String branchToMerge; - private final ObjectId revisionToMerge; - private final Person author; - private final String messageTemplate; - private final String message; - private final boolean sign; - - GitMergeStrategy(Git clone, MergeCommandRequest request, GitContext context, sonia.scm.repository.Repository repository) { - super(clone, context, repository); - this.targetBranch = request.getTargetBranch(); - this.branchToMerge = request.getBranchToMerge(); - this.author = request.getAuthor(); - this.messageTemplate = request.getMessageTemplate(); - this.message = request.getMessage(); - this.sign = request.isSign(); - try { - this.targetRevision = resolveRevision(request.getTargetBranch()); - this.revisionToMerge = resolveRevision(request.getBranchToMerge()); - } catch (IOException e) { - throw new InternalRepositoryException(repository, "Could not resolve revisions of target branch or branch to merge", e); - } - } - - MergeResult doMergeInClone(MergeCommand mergeCommand) throws IOException { - MergeResult result; - try { - ObjectId sourceRevision = resolveRevision(branchToMerge); - mergeCommand - .setCommit(false) // we want to set the author manually - .include(branchToMerge, sourceRevision); - - result = mergeCommand.call(); - } catch (GitAPIException e) { - throw new InternalRepositoryException(getContext().getRepository(), "could not merge branch " + branchToMerge + " into " + targetBranch, e); - } - return result; - } - - Optional doCommit() { - logger.debug("merged branch {} into {}", branchToMerge, targetBranch); - return doCommit(determineMessage(), author, sign); - } - - MergeCommandResult createSuccessResult(String newRevision) { - return MergeCommandResult.success(targetRevision.name(), revisionToMerge.name(), newRevision); - } - - ObjectId getTargetRevision() { - return targetRevision; - } - - ObjectId getRevisionToMerge() { - return revisionToMerge; - } - - private String determineMessage() { - if (!Strings.isNullOrEmpty(message)) { - return message; - } else if (!Strings.isNullOrEmpty(messageTemplate)) { - return MessageFormat.format(messageTemplate, branchToMerge, targetBranch); - } else { - return MessageFormat.format(MERGE_COMMIT_MESSAGE_TEMPLATE, branchToMerge, targetBranch); - } - } - - MergeCommandResult analyseFailure(MergeResult result) { - logger.info("could not merge branch {} into {} with merge status '{}' due to ...", branchToMerge, targetBranch, result.getMergeStatus()); - logger.info("... conflicts: {}", result.getConflicts()); - logger.info("... checkout conflicts: {}", result.getCheckoutConflicts()); - logger.info("... failing paths: {}", result.getFailingPaths()); - logger.info("... message: {}", result); - if (result.getConflicts() == null) { - throw new UnexpectedMergeResultException(getRepository(), result); - } - return MergeCommandResult.failure(targetRevision.name(), revisionToMerge.name(), result.getConflicts().keySet()); - } -} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java index b2d45c9701..82b16083a9 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeWithSquash.java @@ -16,36 +16,21 @@ package sonia.scm.repository.spi; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.MergeCommand; -import org.eclipse.jgit.api.MergeResult; -import org.eclipse.jgit.revwalk.RevCommit; -import sonia.scm.NoChangesMadeException; -import sonia.scm.repository.Repository; +import org.eclipse.jgit.lib.ObjectId; +import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.MergeCommandResult; -import java.io.IOException; +class GitMergeWithSquash { -import static sonia.scm.repository.spi.GitRevisionExtractor.extractRevisionFromRevCommit; + private final MergeCommandRequest request; + private final MergeHelper mergeHelper; -class GitMergeWithSquash extends GitMergeStrategy { - - GitMergeWithSquash(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { - super(clone, request, context, repository); + GitMergeWithSquash(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) { + this.request = request; + this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory); } - @Override - MergeCommandResult run() throws IOException { - MergeCommand mergeCommand = getClone().merge(); - mergeCommand.setSquash(true); - MergeResult result = doMergeInClone(mergeCommand); - - if (result.getMergeStatus().isSuccessful()) { - RevCommit revCommit = doCommit().orElseThrow(() -> new NoChangesMadeException(getRepository())); - push(); - return createSuccessResult(extractRevisionFromRevCommit(revCommit)); - } else { - return analyseFailure(result); - } + MergeCommandResult run() { + return mergeHelper.doRecursiveMerge(request, (sourceRevision, targetRevision) -> new ObjectId[]{targetRevision}); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index a9e5dbc80d..815ef7b6de 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -16,198 +16,194 @@ package sonia.scm.repository.spi; -import com.google.common.util.concurrent.Striped; import com.google.inject.assistedinject.Assisted; import jakarta.inject.Inject; -import org.apache.commons.lang.StringUtils; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.attributes.FilterCommandRegistry; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; +import org.eclipse.jgit.attributes.AttributesNode; +import org.eclipse.jgit.attributes.AttributesRule; import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.InvalidPathException; +import org.eclipse.jgit.errors.DirCacheNameConflictException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; import sonia.scm.ConcurrentModificationException; -import sonia.scm.ContextEntry; import sonia.scm.NoChangesMadeException; -import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; -import sonia.scm.repository.GitRepositoryHandler; -import sonia.scm.repository.GitWorkingCopyFactory; +import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; -import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; import sonia.scm.web.lfs.LfsBlobStoreFactory; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.concurrent.locks.Lock; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static sonia.scm.AlreadyExistsException.alreadyExists; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; +import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand { - private static final Striped REGISTER_LOCKS = Striped.lock(5); - - private final GitWorkingCopyFactory workingCopyFactory; + private final RepositoryManager repositoryManager; + private final GitRepositoryHookEventFactory eventFactory; private final LfsBlobStoreFactory lfsBlobStoreFactory; - private final GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider; + + private RevCommit parentCommit; @Inject - GitModifyCommand(@Assisted GitContext context, GitRepositoryHandler repositoryHandler, LfsBlobStoreFactory lfsBlobStoreFactory, GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider) { - this(context, repositoryHandler.getWorkingCopyFactory(), lfsBlobStoreFactory, gitRepositoryConfigStoreProvider); + GitModifyCommand(@Assisted GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) { + super(context); + this.repositoryManager = repositoryManager; + this.eventFactory = eventFactory; + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } - GitModifyCommand(@Assisted GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory, GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider) { - super(context); - this.workingCopyFactory = workingCopyFactory; - this.lfsBlobStoreFactory = lfsBlobStoreFactory; - this.gitRepositoryConfigStoreProvider = gitRepositoryConfigStoreProvider; + private interface TreeChange { + boolean keepOriginalEntry(String path, ObjectId blob); + + default void finish(TreeHelper treeHelper) { + } } @Override public String execute(ModifyCommandRequest request) { - return inClone(clone -> new ModifyWorker(clone, request), workingCopyFactory, request.getBranch()); + try { + org.eclipse.jgit.lib.Repository repository = context.open(); + CommitHelper commitHelper = new CommitHelper(context, repositoryManager, eventFactory); + String branchToChange = request.getBranch() == null ? context.getGlobalConfig().getDefaultBranch() : request.getBranch(); + ObjectId parentCommitId = repository.resolve(GitUtil.getRevString(branchToChange)); + if (parentCommitId == null && request.getBranch() != null && repository.resolve("HEAD") != null) { + throw notFound(entity("Branch", branchToChange).in(this.repository)); + } + if (request.getExpectedRevision() != null && !parentCommitId.name().equals(request.getExpectedRevision())) { + throw new ConcurrentModificationException(entity("Branch", branchToChange).in(this.repository).build()); + } + + InPlaceWorker inPlaceWorker = new InPlaceWorker(repository); + + try (RevWalk revWalk = new RevWalk(repository)) { + parentCommit = parentCommitId == null ? null : revWalk.parseCommit(parentCommitId); + } + + for (ModifyCommandRequest.PartialRequest r : request.getRequests()) { + r.execute(inPlaceWorker); + } + + TreeHelper treeHelper = new TreeHelper(repository); + if (parentCommitId != null) { + treeHelper.initialize(parentCommitId, inPlaceWorker.changes); + } + + inPlaceWorker.finish(treeHelper); + + ObjectId treeId = treeHelper.flush(); + + if (parentCommitId != null) { + if (parentCommit.getTree().equals(treeId)) { + throw new NoChangesMadeException(GitModifyCommand.this.repository, branchToChange); + } + } + + ObjectId commitId = commitHelper.createCommit( + treeId, + request.getAuthor(), + request.getAuthor(), + request.getCommitMessage(), + request.isSign(), + parentCommitId == null ? new ObjectId[0] : new ObjectId[]{parentCommitId} + ); + + commitHelper.updateBranch(branchToChange, commitId, parentCommitId); + + return commitId.name(); + } catch (IOException | CanceledException | UnsupportedSigningFormatException e) { + throw new InternalRepositoryException(repository, "Error during modification", e); + } } - private class ModifyWorker extends GitCloneWorker implements ModifyWorkerHelper { + private static String removeStartingSlash(String toBeCreated) { + return toBeCreated.startsWith("/") ? toBeCreated.substring(1) : toBeCreated; + } - private final File workDir; - private final ModifyCommandRequest request; + private class TreeHelper { - ModifyWorker(Git clone, ModifyCommandRequest request) { - super(clone, context, repository); - this.workDir = clone.getRepository().getWorkTree(); - this.request = request; + private final org.eclipse.jgit.lib.Repository repository; + private final DirCacheBuilder builder; + private final ObjectInserter inserter; + private final DirCache dirCache = DirCache.newInCore(); + + TreeHelper(Repository repository) { + this.repository = repository; + this.inserter = repository.newObjectInserter(); + this.builder = dirCache.builder(); } - @Override - String run() throws IOException { - getClone().getRepository().getFullBranch(); + private void initialize(ObjectId parentCommitId, Collection changes) throws IOException { + ObjectId parentTreeId = getTreeId(parentCommitId); + try (TreeWalk treeWalk = new TreeWalk(repository)) { - boolean initialCommit = getClone().getRepository().getRefDatabase().getRefs().isEmpty(); + treeWalk.addTree(parentTreeId); + treeWalk.setRecursive(true); - if (!StringUtils.isEmpty(request.getExpectedRevision()) - && !request.getExpectedRevision().equals(getCurrentObjectId().getName())) { - throw new ConcurrentModificationException(ContextEntry.ContextBuilder.entity("Branch", request.getBranch() == null ? "default" : request.getBranch()).in(repository).build()); - } - for (ModifyCommandRequest.PartialRequest r : request.getRequests()) { - r.execute(this); - } - failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())); - Optional revCommit = doCommit(request.getCommitMessage(), request.getAuthor(), request.isSign()); - - if (initialCommit) { - handleBranchForInitialCommit(); - } - - push(); - return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name(); - } - - private void handleBranchForInitialCommit() { - String branch = StringUtils.isNotBlank(request.getBranch()) ? request.getBranch() : context.getGlobalConfig().getDefaultBranch(); - if (StringUtils.isNotBlank(branch)) { - try { - createBranchIfNotThere(branch); - } catch (GitAPIException | IOException e) { - throw new InternalRepositoryException(repository, "could not create default branch for initial commit", e); + while (treeWalk.next()) { + String path = treeWalk.getPathString(); + if (changes.stream().allMatch(c -> c.keepOriginalEntry(path, treeWalk.getObjectId(0)))) { + DirCacheEntry entry = new DirCacheEntry(path); + entry.setObjectId(treeWalk.getObjectId(0)); + entry.setFileMode(treeWalk.getFileMode(0)); + builder.add(entry); + } } } } - private void createBranchIfNotThere(String branch) throws IOException, GitAPIException { - if (!branch.equals(getClone().getRepository().getBranch())) { - getClone().checkout().setName(branch).setCreateBranch(true).call(); - setBranchInConfig(branch); + ObjectId getTreeId(ObjectId commitId) throws IOException { + try (RevWalk revWalk = new RevWalk(repository)) { + RevCommit commit = revWalk.parseCommit(commitId); + return commit.getTree().getId(); } } - private void setBranchInConfig(String branch) { - gitRepositoryConfigStoreProvider.setDefaultBranch(repository, branch); - } - - @Override - public void addFileToScm(String name, Path file) { - addToGitWithLfsSupport(name, file); - } - - private void addToGitWithLfsSupport(String path, Path targetFile) { - REGISTER_LOCKS.get(targetFile).lock(); + void updateTreeWithNewFile(String filePath, ObjectId blobId) { + if (filePath.startsWith("/")) { + filePath = filePath.substring(1); + } try { - LfsBlobStoreCleanFilterFactory cleanFilterFactory = new LfsBlobStoreCleanFilterFactory(lfsBlobStoreFactory, repository, targetFile); - - String registerKey = "git-lfs clean -- '" + path + "'"; - LOG.debug("register lfs filter command factory for command '{}'", registerKey); - FilterCommandRegistry.register(registerKey, cleanFilterFactory::createFilter); - try { - addFileToGit(path); - } catch (GitAPIException e) { - throwInternalRepositoryException("could not add file to index", e); - } finally { - LOG.debug("unregister lfs filter command factory for command \"{}\"", registerKey); - FilterCommandRegistry.unregister(registerKey); - } - } finally { - REGISTER_LOCKS.get(targetFile).unlock(); + DirCacheEntry newEntry = new DirCacheEntry(filePath); + newEntry.setObjectId(blobId); + newEntry.setFileMode(FileMode.REGULAR_FILE); + builder.add(newEntry); + } catch (InvalidPathException e) { + doThrow().violation("Path", filePath).when(true); } } - @Override - public void addMovedFileToScm(String path, Path targetPath) { + ObjectId flush() throws IOException { try { - addFileToGit(path); - } catch (GitAPIException e) { - throwInternalRepositoryException("could not add file to index", e); + builder.finish(); + } catch (DirCacheNameConflictException e) { + throw alreadyExists(entity("File", e.getPath1()).in(GitModifyCommand.this.repository)); } - } - - private void addFileToGit(String toBeCreated) throws GitAPIException { - String toBeCreatedWithoutLeadingSlash = removeStartingPathSeparators(toBeCreated); - DirCache addResult = getClone().add().addFilepattern(toBeCreatedWithoutLeadingSlash).call(); - if (addResult.findEntry(toBeCreatedWithoutLeadingSlash) < 0) { - throw new ModificationFailedException(ContextEntry.ContextBuilder.entity("File", toBeCreated).in(repository).build(), "Could not add file to repository"); - } - } - - @Override - public void doScmDelete(String toBeDeleted) { - try { - String toBeDeletedWithoutLeadingSlash = removeStartingPathSeparators(toBeDeleted); - DirCache deleteResult = getClone().rm().addFilepattern(toBeDeletedWithoutLeadingSlash).call(); - if (deleteResult.findEntry(toBeDeletedWithoutLeadingSlash) >= 0) { - throw new ModificationFailedException(ContextEntry.ContextBuilder.entity("File", toBeDeleted).in(repository).build(), "Could not delete file from repository"); - } - } catch (GitAPIException e) { - throwInternalRepositoryException("could not remove file from index", e); - } - } - - @Override - public boolean isProtectedPath(Path path) { - return path.startsWith(getClone().getRepository().getDirectory().toPath().normalize()); - } - - @Override - public File getWorkDir() { - return workDir; - } - - @Override - public Repository getRepository() { - return repository; - } - - @Override - public String getBranch() { - return request.getBranch(); - } - - private String removeStartingPathSeparators(String path) { - if (path.startsWith("/")) { - return path.substring(1); - } - return path; - } - - private String throwInternalRepositoryException(String message, Exception e) { - throw new InternalRepositoryException(context.getRepository(), message, e); + ObjectId newTreeId = dirCache.writeTree(inserter); + inserter.flush(); + return newTreeId; } } @@ -215,4 +211,245 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman ModifyCommand create(GitContext context); } + private class InPlaceWorker implements Worker { + private final Collection changes = new ArrayList<>(); + private final Repository repository; + private final Map attributesCache = new HashMap<>(); + + public InPlaceWorker(Repository repository) { + this.repository = repository; + } + + @Override + public void delete(String toBeDeleted, boolean recursive) { + changes.add(new DeleteChange(toBeDeleted, recursive)); + } + + @Override + public void create(String toBeCreated, File file, boolean overwrite) throws IOException { + changes.add(new CreateChange(overwrite, toBeCreated, createBlob(toBeCreated, file))); + } + + @Override + public void modify(String toBeModified, File file) throws IOException { + ObjectId blobId = createBlob(toBeModified, file); + changes.add(new ModifyChange(toBeModified, blobId)); + } + + @Override + public void move(String oldPath, String newPath, boolean overwrite) { + changes.add(new MoveChange(oldPath, newPath)); + } + + public void finish(TreeHelper treeHelper) throws IOException { + for (TreeChange c : changes) { + c.finish(treeHelper); + } + } + + private ObjectId createBlob(String path, File file) throws IOException { + + try (ObjectInserter inserter = repository.newObjectInserter()) { + + if (isLfsFile(path)) { + return writeWithLfs(file, inserter); + } else { + ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, file.length(), new FileInputStream(file)); + inserter.flush(); + return blobId; + } + } + } + + private boolean isLfsFile(String path) { + if (parentCommit == null) { + return false; + } + String[] pathParts = path.split("/"); + + for (int i = pathParts.length; i > 0; --i) { + String directory = i == 1 ? "" : String.join("/", Arrays.copyOf(pathParts, i - 1)) + "/"; + String relativeFileName = path.substring(directory.length()); + if (isLfsFile(directory, relativeFileName)) { + return true; + } + } + return false; + } + + private boolean isLfsFile(String directory, String relativeFileName) { + String attributesPath = directory + ".gitattributes"; + + ObjectId treeId = parentCommit.getTree().getId(); + + return attributesCache + .computeIfAbsent(directory, dir -> loadAttributes(treeId, attributesPath)) + .getRules() + .stream() + .anyMatch(attributes -> hasLfsFilterAttribute(relativeFileName, attributes)); + } + + private boolean hasLfsFilterAttribute(String relativeFileName, AttributesRule attributes) { + if (attributes.isMatch(relativeFileName, false)) { + return attributes.getAttributes().stream().anyMatch(attribute -> attribute.getKey().equals("filter") && attribute.getValue().equals("lfs")); + } + return false; + } + + private AttributesNode loadAttributes(ObjectId treeId, String attributesPath) { + try (TreeWalk treeWalk = new TreeWalk(repository)) { + treeWalk.addTree(treeId); + treeWalk.setRecursive(true); + treeWalk.setFilter(PathFilter.create(attributesPath)); + + AttributesNode attributesNode = new AttributesNode(); + if (treeWalk.next()) { + ObjectId objectId = treeWalk.getObjectId(0); + ObjectLoader loader = repository.open(objectId); + attributesNode.parse(loader.openStream()); + } + return attributesNode; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private ObjectId writeWithLfs(File file, ObjectInserter inserter) throws IOException { + LfsBlobStoreCleanFilterFactory cleanFilterFactory = new LfsBlobStoreCleanFilterFactory(lfsBlobStoreFactory, GitModifyCommand.this.repository, file.toPath()); + ByteArrayOutputStream pointer = new ByteArrayOutputStream(); + cleanFilterFactory.createFilter(repository, new FileInputStream(file), pointer).run(); + ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, pointer.toByteArray()); + inserter.flush(); + return blobId; + } + + private class DeleteChange implements TreeChange { + private final String toBeDeleted; + private final boolean recursive; + private final String toBeDeletedAsDirectory; + private boolean foundOriginal; + + public DeleteChange(String toBeDeleted, boolean recursive) { + this.toBeDeleted = removeStartingSlash(toBeDeleted); + this.recursive = recursive; + this.toBeDeletedAsDirectory = this.toBeDeleted + "/"; + } + + @Override + public boolean keepOriginalEntry(String path, ObjectId blob) { + if (path.equals(toBeDeleted) || recursive && path.startsWith(toBeDeletedAsDirectory)) { + foundOriginal = true; + return false; + } + return true; + } + + @Override + public void finish(TreeHelper treeHelper) { + if (!foundOriginal) { + throw notFound(entity("File", toBeDeleted).in(GitModifyCommand.this.repository)); + } + } + } + + private class CreateChange implements TreeChange { + private final String toBeCreated; + private final boolean overwrite; + private final ObjectId blobId; + + public CreateChange(boolean overwrite, String toBeCreated, ObjectId blobId) { + this.toBeCreated = removeStartingSlash(toBeCreated); + this.overwrite = overwrite; + this.blobId = blobId; + } + + @Override + public boolean keepOriginalEntry(String path, ObjectId blob) { + if (path.equals(toBeCreated)) { + if (!overwrite) { + throw alreadyExists(entity("File", toBeCreated).in(GitModifyCommand.this.repository)); + } + return false; + } + return true; + } + + @Override + public void finish(TreeHelper treeHelper) { + treeHelper.updateTreeWithNewFile(toBeCreated, blobId); + } + } + + private class ModifyChange implements TreeChange { + private final String toBeModified; + private final ObjectId blobId; + private boolean foundOriginal; + + public ModifyChange(String toBeModified, ObjectId blobId) { + this.toBeModified = removeStartingSlash(toBeModified); + this.blobId = blobId; + } + + @Override + public boolean keepOriginalEntry(String path, ObjectId blob) { + if (path.equals(toBeModified)) { + foundOriginal = true; + return false; + } + return true; + } + + @Override + public void finish(TreeHelper treeHelper) { + if (!foundOriginal) { + throw notFound(entity("File", toBeModified).in(GitModifyCommand.this.repository)); + } + treeHelper.updateTreeWithNewFile(toBeModified, blobId); + } + } + + private class MoveChange implements TreeChange { + private final String oldPath; + private final String oldPathAsDirectory; + private final String newPath; + private final Collection moves = new ArrayList<>(); + + public MoveChange(String oldPath, String newPath) { + this.oldPath = removeStartingSlash(oldPath); + this.newPath = removeStartingSlash(newPath); + this.oldPathAsDirectory = this.oldPath + "/"; + } + + @Override + public boolean keepOriginalEntry(String path, ObjectId blob) { + if (path.equals(oldPath) || path.startsWith(oldPathAsDirectory)) { + moves.add(new Move(path, blob)); + return false; + } + return !path.equals(newPath); + } + + @Override + public void finish(TreeHelper treeHelper) { + if (moves.isEmpty()) { + throw notFound(entity("File", oldPath).in(GitModifyCommand.this.repository)); + } + moves.forEach(move -> move.move(treeHelper)); + } + + private class Move { + private final String to; + private final ObjectId blobId; + + private Move(String from, ObjectId blobId) { + this.to = from.replace(oldPath, newPath); + this.blobId = blobId; + } + + private void move(TreeHelper treeHelper) { + treeHelper.updateTreeWithNewFile(to, blobId); + } + } + } + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryHookEventFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryHookEventFactory.java index 605bfcde6e..f729ba1f6d 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryHookEventFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryHookEventFactory.java @@ -17,6 +17,7 @@ package sonia.scm.repository.spi; import jakarta.inject.Inject; +import org.eclipse.jgit.revwalk.RevCommit; import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.RepositoryHookEvent; @@ -24,10 +25,11 @@ import sonia.scm.repository.Tag; import sonia.scm.repository.api.HookContext; import sonia.scm.repository.api.HookContextFactory; -import java.io.IOException; import java.util.List; +import java.util.function.Supplier; import static sonia.scm.repository.RepositoryHookType.POST_RECEIVE; +import static sonia.scm.repository.RepositoryHookType.PRE_RECEIVE; class GitRepositoryHookEventFactory { @@ -40,14 +42,23 @@ class GitRepositoryHookEventFactory { this.changesetConverterFactory = changesetConverterFactory; } - RepositoryHookEvent createEvent(GitContext gitContext, + RepositoryHookEvent createPostReceiveEvent(GitContext gitContext, List branches, List tags, - GitLazyChangesetResolver changesetResolver - ) throws IOException { + Supplier> changesetResolver) { GitChangesetConverter converter = changesetConverterFactory.create(gitContext.open()); GitImportHookContextProvider contextProvider = new GitImportHookContextProvider(converter, branches, tags, changesetResolver); HookContext context = hookContextFactory.createContext(contextProvider, gitContext.getRepository()); return new RepositoryHookEvent(context, gitContext.getRepository(), POST_RECEIVE); } + + RepositoryHookEvent createPreReceiveEvent(GitContext gitContext, + List branches, + List tags, + Supplier> changesetResolver) { + GitChangesetConverter converter = changesetConverterFactory.create(gitContext.open()); + GitImportHookContextProvider contextProvider = new GitImportHookContextProvider(converter, branches, tags, changesetResolver); + HookContext context = hookContextFactory.createContext(contextProvider, gitContext.getRepository()); + return new RepositoryHookEvent(context, gitContext.getRepository(), PRE_RECEIVE); + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagCommand.java index ab4f219c61..aff083c6ea 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagCommand.java @@ -230,12 +230,7 @@ public class GitTagCommand extends AbstractGitCommand implements TagCommand { .map(tag -> new ReceiveCommand(fromString(tag.getRevision()), zeroId(), REFS_TAGS_PREFIX + tag.getName())) .forEach(receiveCommands::add); return x -> { - Repository gitRepo; - try { - gitRepo = context.open(); - } catch (IOException e) { - throw new InternalRepositoryException(repository, "failed to open repository for post receive hook after internal change", e); - } + Repository gitRepo = context.open(); GitHookChangesetCollector collector = GitHookChangesetCollector.collectChangesets( converterFactory, diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitUnbundleCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitUnbundleCommand.java index 45bd5b6c20..fa81c3ab3f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitUnbundleCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitUnbundleCommand.java @@ -73,11 +73,11 @@ public class GitUnbundleCommand extends AbstractGitCommand implements UnbundleCo List branches = extractBranches(git); List tags = extractTags(git); GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git); - RepositoryHookEvent event = eventFactory.createEvent(context, branches, tags, changesetResolver); + RepositoryHookEvent event = eventFactory.createPostReceiveEvent(context, branches, tags, changesetResolver); if (event != null) { request.getPostEventSink().accept(event); } - } catch (IOException | GitAPIException e) { + } catch (GitAPIException e) { throw new ImportFailedException( ContextEntry.ContextBuilder.entity(context.getRepository()).build(), "Could not fire post receive repository hook event after unbundle", diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/MergeHelper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/MergeHelper.java new file mode 100644 index 0000000000..8422f86665 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/MergeHelper.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2020 - present Cloudogu GmbH + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package sonia.scm.repository.spi; + +import com.google.common.base.Strings; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.merge.ResolveMerger; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import sonia.scm.NoChangesMadeException; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.api.MergeCommandResult; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.Map; +import java.util.function.BiFunction; + +import static org.eclipse.jgit.merge.MergeStrategy.RECURSIVE; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; + +@Slf4j +class MergeHelper { + + private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n", + "Merge of branch {0} into {1}", + "", + "Automatic merge by SCM-Manager."); + + private final GitContext context; + private final RepositoryManager repositoryManager; + private final GitRepositoryHookEventFactory eventFactory; + private final Repository repository; + + private final ObjectId targetRevision; + private final ObjectId revisionToMerge; + private final String targetBranch; + private final String branchToMerge; + private final String messageTemplate; + private final String message; + + MergeHelper(GitContext context, + MergeCommandRequest request, + RepositoryManager repositoryManager, + GitRepositoryHookEventFactory eventFactory) { + this.context = context; + this.repositoryManager = repositoryManager; + this.eventFactory = eventFactory; + try { + this.repository = context.open(); + this.targetRevision = resolveRevision(request.getTargetBranch()); + this.revisionToMerge = resolveRevision(request.getBranchToMerge()); + } catch (IOException e) { + throw new InternalRepositoryException(context.getRepository(), "Could not resolve revisions of target branch or branch to merge", e); + } + this.targetBranch = request.getTargetBranch(); + this.branchToMerge = request.getBranchToMerge(); + this.messageTemplate = request.getMessageTemplate(); + this.message = request.getMessage(); + } + + ObjectId getTargetRevision() { + return targetRevision; + } + + ObjectId getRevisionToMerge() { + return revisionToMerge; + } + + ObjectId resolveRevision(String revision) throws IOException { + ObjectId resolved = repository.resolve(revision); + if (resolved == null) { + throw notFound(entity("Revision", revision).in(context.getRepository())); + } else { + return resolved; + } + } + + String determineMessage() { + if (!Strings.isNullOrEmpty(message)) { + return message; + } else if (!Strings.isNullOrEmpty(messageTemplate)) { + return MessageFormat.format(messageTemplate, branchToMerge, targetBranch); + } else { + return MessageFormat.format(MERGE_COMMIT_MESSAGE_TEMPLATE, branchToMerge, targetBranch); + } + } + + Collection getFailingPaths(ResolveMerger merger) { + return merger.getMergeResults() + .entrySet() + .stream() + .filter(entry -> entry.getValue().containsConflicts()) + .map(Map.Entry::getKey) + .toList(); + } + + boolean isMergedInto(ObjectId baseRevision, ObjectId revisionToCheck) { + try (RevWalk revWalk = new RevWalk(context.open())) { + RevCommit baseCommit = revWalk.parseCommit(baseRevision); + RevCommit commitToCheck = revWalk.parseCommit(revisionToCheck); + return revWalk.isMergedInto(baseCommit, commitToCheck); + } catch (IOException e) { + throw new InternalRepositoryException(context.getRepository(), "failed to check whether revision " + revisionToCheck + " is merged into " + baseRevision, e); + } + } + + MergeCommandResult doRecursiveMerge(MergeCommandRequest request, BiFunction parents) { + log.trace("merge branch {} into {}", branchToMerge, targetBranch); + try { + org.eclipse.jgit.lib.Repository repository = context.open(); + ObjectId sourceRevision = getRevisionToMerge(); + ObjectId targetRevision = getTargetRevision(); + + assertBranchesNotMerged(request, sourceRevision, targetRevision); + + ResolveMerger merger = (ResolveMerger) RECURSIVE.newMerger(repository, true); // The recursive merger is always a RecursiveMerge + boolean mergeSucceeded = merger.merge(sourceRevision, targetRevision); + if (!mergeSucceeded) { + log.trace("could not merge branch {} into {}", branchToMerge, targetBranch); + return MergeCommandResult.failure(targetRevision.name(), sourceRevision.name(), getFailingPaths(merger)); + } + ObjectId newTreeId = merger.getResultTreeId(); + log.trace("create commit for new tree {}", newTreeId); + + CommitHelper commitHelper = new CommitHelper(context, repositoryManager, eventFactory); + ObjectId commitId = commitHelper.createCommit( + newTreeId, + request.getAuthor(), + request.getAuthor(), + determineMessage(), + request.isSign(), + parents.apply(sourceRevision, targetRevision) + ); + log.trace("created commit {}", commitId); + + commitHelper.updateBranch(request.getTargetBranch(), commitId, targetRevision); + + return MergeCommandResult.success(targetRevision.name(), sourceRevision.name(), commitId.name()); + } catch (IOException | CanceledException | UnsupportedSigningFormatException e) { + throw new InternalRepositoryException(context.getRepository(), "Error during merge", e); + } + } + + private void assertBranchesNotMerged(MergeCommandRequest request, ObjectId sourceRevision, ObjectId targetRevision) throws IOException { + if (isMergedInto(sourceRevision, targetRevision)) { + throw new NoChangesMadeException(context.getRepository(), request.getTargetBranch()); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/PostReceiveRepositoryHookEventFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/PostReceiveRepositoryHookEventFactory.java index 90af25f255..19c63b6cee 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/PostReceiveRepositoryHookEventFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/PostReceiveRepositoryHookEventFactory.java @@ -20,14 +20,11 @@ import com.google.inject.assistedinject.Assisted; import jakarta.inject.Inject; 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.Tag; import sonia.scm.repository.WrappedRepositoryHookEvent; -import sonia.scm.repository.api.ImportFailedException; -import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @@ -46,18 +43,10 @@ public class PostReceiveRepositoryHookEventFactory { void fireForFetch(Git git, FetchResult result) { PostReceiveRepositoryHookEvent event; - try { - List branches = getBranchesFromFetchResult(result); - List 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 - ); - } + List branches = getBranchesFromFetchResult(result); + List tags = getTagsFromFetchResult(result); + GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git); + event = new PostReceiveRepositoryHookEvent(WrappedRepositoryHookEvent.wrap(eventFactory.createPostReceiveEvent(context, branches, tags, changesetResolver))); eventBus.post(event); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java index 5f1403fa54..29887052b1 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitChangesetConverterTest.java @@ -29,13 +29,17 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.GpgSignature; -import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Signer; +import org.eclipse.jgit.lib.Signers; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.CredentialsProvider; import org.junit.jupiter.api.AfterAll; @@ -134,21 +138,21 @@ class GitChangesetConverterTest { private PublicKey publicKey; private PGPKeyPair keyPair; - private GpgSigner defaultSigner; + private Signer defaultSigner; @BeforeEach void setUpTestingSignerAndCaptureDefault() throws Exception { - defaultSigner = GpgSigner.getDefault(); + defaultSigner = Signers.get(GpgConfig.GpgFormat.OPENPGP); // we use the same keypair for all tests to speed things up a little bit if (keyPair == null) { keyPair = createKeyPair(); - GpgSigner.setDefault(new TestingGpgSigner(keyPair)); + Signers.set(GpgConfig.GpgFormat.OPENPGP, new TestingGpgSigner(keyPair)); } } @AfterEach void restoreDefaultSigner() { - GpgSigner.setDefault(defaultSigner); + Signers.set(GpgConfig.GpgFormat.OPENPGP, defaultSigner); } @Test @@ -242,7 +246,7 @@ class GitChangesetConverterTest { return new JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, pair, new Date()); } - private static class TestingGpgSigner extends GpgSigner { + private static class TestingGpgSigner implements Signer { private final PGPKeyPair keyPair; @@ -251,13 +255,7 @@ class GitChangesetConverterTest { } @Override - public boolean canLocateSigningKey(String gpgSigningKey, PersonIdent committer, CredentialsProvider credentialsProvider) { - return true; - } - - @Override - public void sign(CommitBuilder commit, String gpgSigningKey, - PersonIdent committer, CredentialsProvider credentialsProvider) { + public GpgSignature sign(Repository repository, GpgConfig gpgConfig, byte[] bytes, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) throws CanceledException, IOException, UnsupportedSigningFormatException { try { if (keyPair == null) { throw new JGitInternalException(JGitText.get().unableToSignCommitNoSecretKey); @@ -274,15 +272,18 @@ class GitChangesetConverterTest { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) { - signatureGenerator.update(commit.build()); signatureGenerator.generate().encode(out); } - commit.setGpgSignature(new GpgSignature(buffer.toByteArray())); + return new GpgSignature(buffer.toByteArray()); } catch (PGPException | IOException e) { throw new JGitInternalException(e.getMessage(), e); } } + @Override + public boolean canLocateSigningKey(Repository repository, GpgConfig gpgConfig, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) throws CanceledException { + return true; + } } // register bouncy castle provider on load diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java index e13fe90afe..316636dc78 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java @@ -16,11 +16,11 @@ package sonia.scm.repository; -import org.eclipse.jgit.api.errors.CanceledException; -import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.GpgSignature; -import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Signer; import org.eclipse.jgit.transport.CredentialsProvider; import sonia.scm.security.GPG; import sonia.scm.security.PrivateKey; @@ -38,20 +38,19 @@ public final class GitTestHelper { return new GitChangesetConverterFactory(new NoopGPG()); } - public static class SimpleGpgSigner extends GpgSigner { + public static class SimpleGpgSigner implements Signer { public static byte[] getSignature() { return "SIGNATURE".getBytes(); } @Override - public void sign(CommitBuilder commitBuilder, String s, PersonIdent personIdent, CredentialsProvider - credentialsProvider) throws CanceledException { - commitBuilder.setGpgSignature(new GpgSignature(SimpleGpgSigner.getSignature())); + public GpgSignature sign(Repository repository, GpgConfig gpgConfig, byte[] bytes, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) { + return new GpgSignature(SimpleGpgSigner.getSignature()); } @Override - public boolean canLocateSigningKey(String s, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { + public boolean canLocateSigningKey(Repository repository, GpgConfig gpgConfig, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) { return true; } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/ScmGpgSignerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/ScmGpgSignerTest.java index 47caba200d..ea9378b07a 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/ScmGpgSignerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/ScmGpgSignerTest.java @@ -18,12 +18,9 @@ package sonia.scm.repository; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.CanceledException; -import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; -import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; -import org.eclipse.jgit.lib.CommitBuilder; -import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Signers; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.CredentialsProvider; import org.junit.jupiter.api.BeforeEach; @@ -79,7 +76,7 @@ class ScmGpgSignerTest { when(gpg.getPrivateKey()).thenReturn(privateKey); - GpgSigner.setDefault(signer); + Signers.set(GpgConfig.GpgFormat.OPENPGP, signer); Path repositoryPath = workdir.resolve("repository"); Git git = Git.init().setDirectory(repositoryPath.toFile()).call(); @@ -103,6 +100,6 @@ class ScmGpgSignerTest { @Test void canLocateSigningKey() throws CanceledException { - assertThat(signer.canLocateSigningKey("foo", personIdent, credentialsProvider)).isTrue(); + assertThat(signer.canLocateSigningKey(null, null, personIdent, "foo", credentialsProvider)).isTrue(); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java index f11b7b653b..097162a788 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java @@ -41,7 +41,9 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase { if (context == null) { - context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()), new GitConfig()); + GitConfig config = new GitConfig(); + config.setDefaultBranch("master"); + context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()), config); } return context; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLazyChangesetResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLazyChangesetResolverTest.java index 874014544f..3884b54a1e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLazyChangesetResolverTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLazyChangesetResolverTest.java @@ -34,7 +34,7 @@ public class GitLazyChangesetResolverTest extends AbstractGitCommandTestBase { @Test public void shouldResolveChangesets() throws IOException { GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(repository, Git.wrap(createContext().open())); - Iterable commits = changesetResolver.call(); + Iterable commits = changesetResolver.get(); RevCommit firstCommit = commits.iterator().next(); assertThat(firstCommit.getId().toString()).isEqualTo("commit a8495c0335a13e6e432df90b3727fa91943189a7 1602078219 -----sp"); @@ -46,7 +46,7 @@ public class GitLazyChangesetResolverTest extends AbstractGitCommandTestBase { public void shouldResolveAllChangesets() throws IOException, GitAPIException { Git git = Git.wrap(createContext().open()); GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(repository, git); - Iterable allCommits = changesetResolver.call(); + Iterable allCommits = changesetResolver.get(); int allCommitsCounter = Iterables.size(allCommits); int singleBranchCommitsCounter = Iterables.size(git.log().call()); @@ -57,7 +57,7 @@ public class GitLazyChangesetResolverTest extends AbstractGitCommandTestBase { public void shouldThrowImportFailedException() { Git git = mock(Git.class); doThrow(ImportFailedException.class).when(git).log(); - new GitLazyChangesetResolver(repository, git).call(); + new GitLazyChangesetResolver(repository, git).get(); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandConflictTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandConflictTest.java index 5d8d8be25e..0246d95449 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandConflictTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandConflictTest.java @@ -90,7 +90,7 @@ public class GitMergeCommandConflictTest extends AbstractGitCommandTestBase { private MergeConflictResult computeMergeConflictResult(String branchToMerge, String targetBranch) { AttributeAnalyzer attributeAnalyzer = mock(AttributeAnalyzer.class); when(attributeAnalyzer.hasExternalMergeToolConflicts(any(), any())).thenReturn(false); - GitMergeCommand gitMergeCommand = new GitMergeCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider(null, repositoryLocationResolver)), new SimpleMeterRegistry()), attributeAnalyzer); + GitMergeCommand gitMergeCommand = new GitMergeCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider(null, repositoryLocationResolver)), new SimpleMeterRegistry()), attributeAnalyzer, null, null); MergeCommandRequest mergeCommandRequest = new MergeCommandRequest(); mergeCommandRequest.setBranchToMerge(branchToMerge); mergeCommandRequest.setTargetBranch(targetBranch); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index 487db356b0..c3f460a0d4 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -23,24 +23,28 @@ import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Signers; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; -import org.junit.jupiter.api.Assertions; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.ConcurrentModificationException; import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.repository.Added; import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.Person; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult; import sonia.scm.repository.api.MergePreventReason; @@ -50,15 +54,17 @@ import sonia.scm.repository.work.NoneCachingWorkingCopyPool; import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.user.User; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; import java.io.IOException; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -69,14 +75,16 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { @Rule public ShiroRule shiro = new ShiroRule(); - @Rule - public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); @Mock private AttributeAnalyzer attributeAnalyzer; + @Mock + private RepositoryManager repositoryManager; + @Mock + private GitRepositoryHookEventFactory eventFactory; @BeforeClass public static void setSigner() { - GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); + Signers.set(GpgConfig.GpgFormat.OPENPGP, new GitTestHelper.SimpleGpgSigner()); } @Test @@ -248,27 +256,26 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt"); } - @Test - public void shouldHandleUnexpectedMergeResults() { - GitMergeCommand command = createCommand(git -> { - try { - FileWriter fw = new FileWriter(new File(git.getRepository().getWorkTree(), "b.txt"), true); - BufferedWriter bw = new BufferedWriter(fw); - bw.write("change"); - bw.newLine(); - bw.close(); - } catch (IOException e) { - e.printStackTrace(); - } - }); + @Test(expected = ConcurrentModificationException.class) + public void shouldHandleConcurrentBranchModification() { + GitMergeCommand command = createCommand(); MergeCommandRequest request = new MergeCommandRequest(); - request.setBranchToMerge("mergeable"); request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); - request.setMessageTemplate("simple"); - Assertions.assertThrows(UnexpectedMergeResultException.class, () -> command.merge(request)); + // create concurrent modification after the pre commit hook was fired + doAnswer(invocation -> { + RefUpdate refUpdate = createCommand() + .open() + .updateRef("refs/heads/master"); + refUpdate.setNewObjectId(ObjectId.fromString("2f95f02d9c568594d31e78464bd11a96c62e3f91")); + refUpdate.update(); + return null; + }).when(repositoryManager).fireHookEvent(any()); + + command.merge(request); } @Test @@ -344,6 +351,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getParentCount()).isEqualTo(1); PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); String message = mergeCommit.getFullMessage(); assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); @@ -370,6 +378,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { String message = mergeCommit.getFullMessage(); assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); assertThat(message).isEqualTo("squash three commits"); + assertThat(mergeCommit.getParentCount()).isEqualTo(1); + assertThat(mergeCommit.getParent(0).name()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext()); List changes = modificationsCommand.getModifications("master").getAdded(); @@ -535,6 +545,32 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { assertThat(mergeCommit.getName()).doesNotStartWith("91b99de908fcd04772798a31c308a64aea1a5523"); } + @Test + public void shouldRebaseMultipleCommits() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("squash"); + request.setMergeStrategy(MergeStrategy.REBASE); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.merge(request); + + Repository repository = createContext().open(); + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(6).call(); + + assertThat(commits) + .extracting("shortMessage") + .containsExactly( + "third", + "second commit", + "first commit", + "added new line for blame", + "added file f", + "added file d and e in folder c" + ); + } + @Test public void shouldRejectRebaseMergeIfBranchCannotBeRebased() throws IOException, GitAPIException { GitMergeCommand command = createCommand(); @@ -547,11 +583,31 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { MergeCommandResult mergeCommandResult = command.merge(request); assertThat(mergeCommandResult.isSuccess()).isFalse(); + assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt"); Repository repository = createContext().open(); Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); RevCommit headCommit = commits.iterator().next(); assertThat(headCommit.getName()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); + } + @Test + public void shouldFireEvents() { + RepositoryHookEvent preReceive = mock(RepositoryHookEvent.class); + RepositoryHookEvent postReceive = mock(RepositoryHookEvent.class); + when(eventFactory.createPreReceiveEvent(any(), eq(List.of("master")), any(), any())).thenReturn(preReceive); + when(eventFactory.createPostReceiveEvent(any(), eq(List.of("master")), any(), any())).thenReturn(postReceive); + + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("mergeable"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.merge(request); + + verify(repositoryManager).fireHookEvent(preReceive); + verify(repositoryManager).fireHookEvent(postReceive); } private GitMergeCommand createCommand() { @@ -560,7 +616,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { } private GitMergeCommand createCommand(Consumer interceptor) { - return new GitMergeCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider(null, repositoryLocationResolver)), new SimpleMeterRegistry()), attributeAnalyzer) { + return new GitMergeCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider(null, repositoryLocationResolver)), new SimpleMeterRegistry()), attributeAnalyzer, repositoryManager, eventFactory) { @Override > R inClone(Function workerSupplier, GitWorkingCopyFactory workingCopyFactory, String initialBranch) { Function interceptedWorkerSupplier = git -> { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index 616c4a15e6..ad6d2bc395 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -16,12 +16,12 @@ package sonia.scm.repository.spi; -import com.github.sdorra.shiro.SubjectAware; -import org.apache.shiro.subject.SimplePrincipalCollection; -import org.apache.shiro.subject.Subject; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; @@ -29,12 +29,12 @@ import org.junit.Test; import sonia.scm.AlreadyExistsException; import sonia.scm.BadRequestException; import sonia.scm.ConcurrentModificationException; +import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.ScmConstraintViolationException; import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.Person; import sonia.scm.repository.RepositoryHookType; -import sonia.scm.user.User; import java.io.File; import java.io.IOException; @@ -45,14 +45,14 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.description; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; public class GitModifyCommandTest extends GitModifyCommandTestBase { - private static final String REALM = "AdminRealm"; - @Override protected String getZippedRepositoryResource() { return "sonia/scm/repository/spi/scm-git-spi-move-test.zip"; @@ -263,6 +263,38 @@ public class GitModifyCommandTest extends GitModifyCommandTestBase { } } + @Test + public void shouldDeleteDirectoryButNotFileWithSamePrefix() throws IOException { + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = prepareModifyCommandRequest(); + request.setBranch("similar-paths"); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("c", true)); + + command.execute(request); + + boolean foundCTxt = false; + + Repository repository = createContext().open(); + ObjectId lastCommit = repository.resolve("refs/heads/similar-paths"); + try (RevWalk walk = new RevWalk(repository)) { + RevCommit commit = walk.parseCommit(lastCommit); + ObjectId treeId = commit.getTree().getId(); + TreeWalk treeWalk = new TreeWalk(repository); + treeWalk.setRecursive(true); + treeWalk.addTree(treeId); + while (treeWalk.next()) { + if (treeWalk.getPathString().startsWith("c/")) { + fail("directory should be deleted"); + } + if (treeWalk.getPathString().equals("c.txt")) { + foundCTxt = true; + } + } + } + assertThat(foundCTxt).isTrue(); + } + @Test(expected = NotFoundException.class) public void shouldThrowNotFoundExceptionWhenFileToDeleteDoesNotExist() { GitModifyCommand command = createCommand(); @@ -346,10 +378,10 @@ public class GitModifyCommandTest extends GitModifyCommandTestBase { command.execute(request); - verify(transportProtocolRule.repositoryManager, description("pre receive hook event expected")) + verify(repositoryManager, description("pre receive hook event expected")) .fireHookEvent(argThat(argument -> argument.getType() == RepositoryHookType.PRE_RECEIVE)); await().pollInterval(50, MILLISECONDS).atMost(1, SECONDS).untilAsserted(() -> - verify(transportProtocolRule.repositoryManager, description("post receive hook event expected")) + verify(repositoryManager, description("post receive hook event expected")) .fireHookEvent(argThat(argument -> argument.getType() == RepositoryHookType.POST_RECEIVE)) ); } @@ -511,7 +543,7 @@ public class GitModifyCommandTest extends GitModifyCommandTestBase { assertInTree(assertions); } - @Test(expected = AlreadyExistsException.class) + @Test(expected = NoChangesMadeException.class) public void shouldFailMoveAndKeepFilesWhenSourceAndTargetAreTheSame() { GitModifyCommand command = createCommand(); @@ -521,18 +553,31 @@ public class GitModifyCommandTest extends GitModifyCommandTestBase { command.execute(request); } + @Test(expected = ConcurrentModificationException.class) + public void shouldFailOnConcurrent() throws IOException, GitAPIException { + File newFile = Files.write(tempFolder.newFile().toPath(), "new content".getBytes()).toFile(); + GitModifyCommand command = createCommand(); + ModifyCommandRequest request = prepareModifyCommandRequest(); + request.setBranch("master"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + + // create concurrent modification after the pre commit hook was fired + doAnswer(invocation -> { + RefUpdate refUpdate = createCommand() + .open() + .updateRef("refs/heads/master"); + refUpdate.setNewObjectId(ObjectId.fromString("a7d622087b6847725670ae84fa37bdf451123008")); + refUpdate.update(); + return null; + }).when(repositoryManager).fireHookEvent(any()); + + command.execute(request); + } + private ModifyCommandRequest prepareModifyCommandRequest() { ModifyCommandRequest request = new ModifyCommandRequest(); request.setCommitMessage("Make some change"); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); return request; } - - private ModifyCommandRequest prepareModifyCommandRequestWithoutAuthorEmail() { - ModifyCommandRequest request = new ModifyCommandRequest(); - request.setAuthor(new Person("Dirk Gently", "")); - request.setCommitMessage("Make some change"); - return request; - } - } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTestBase.java index 3dba080ba3..21b1f2dc1f 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTestBase.java @@ -18,39 +18,42 @@ package sonia.scm.repository.spi; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Signers; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.BeforeClass; import org.junit.Rule; import sonia.scm.repository.GitTestHelper; -import sonia.scm.repository.work.NoneCachingWorkingCopyPool; -import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.RepositoryHookType; +import sonia.scm.repository.RepositoryManager; import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.IOException; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static sonia.scm.repository.spi.GitRepositoryConfigStoreProviderTestUtil.createGitRepositoryConfigStoreProvider; +import static org.mockito.Mockito.when; +import static sonia.scm.repository.RepositoryHookType.POST_RECEIVE; +import static sonia.scm.repository.RepositoryHookType.PRE_RECEIVE; @SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") class GitModifyCommandTestBase extends AbstractGitCommandTestBase { - @Rule - public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); @Rule public ShiroRule shiro = new ShiroRule(); final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + final RepositoryManager repositoryManager = mock(RepositoryManager.class); @BeforeClass public static void setSigner() { - GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); + Signers.set(GpgConfig.GpgFormat.OPENPGP, new GitTestHelper.SimpleGpgSigner()); } RevCommit getLastCommit(Git git) throws GitAPIException, IOException { @@ -58,11 +61,23 @@ class GitModifyCommandTestBase extends AbstractGitCommandTestBase { } GitModifyCommand createCommand() { + GitRepositoryHookEventFactory eventFactory = mock(GitRepositoryHookEventFactory.class); + RepositoryHookEvent preReceiveEvent = mockEvent(PRE_RECEIVE); + when(eventFactory.createPreReceiveEvent(any(), any(), any(), any())).thenReturn(preReceiveEvent); + RepositoryHookEvent postReceiveEvent = mockEvent(POST_RECEIVE); + when(eventFactory.createPostReceiveEvent(any(), any(), any(), any())).thenReturn(postReceiveEvent); return new GitModifyCommand( createContext(), - new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider(null, repositoryLocationResolver)), new SimpleMeterRegistry()), lfsBlobStoreFactory, - createGitRepositoryConfigStoreProvider()); + repositoryManager, + eventFactory + ); + } + + private static RepositoryHookEvent mockEvent(RepositoryHookType type) { + RepositoryHookEvent mock = mock(RepositoryHookEvent.class); + when(mock.getType()).thenReturn(type); + return mock; } void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java index 6f662f1503..1ecbc0d681 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java @@ -68,6 +68,36 @@ public class GitModifyCommand_LFSTest extends GitModifyCommandTestBase { assertThat(outputStream).hasToString("new content"); } + @Test + public void shouldCreateCommitInSubdirectoryWithAttributesInSamePath() throws IOException, GitAPIException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String newRef = createCommit("jpegs/new_lfs.jpg", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", outputStream); + + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getFullMessage()).isEqualTo("test commit"); + assertThat(lastCommit.getAuthorIdent().getName()).isEqualTo("Dirk Gently"); + assertThat(newRef).isEqualTo(lastCommit.toObjectId().name()); + } + + assertThat(outputStream).hasToString("new content"); + } + + @Test + public void shouldCreateCommitInSubdirectoryWithAttributesInParentPath() throws IOException, GitAPIException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String newRef = createCommit("jpegs/new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", outputStream); + + try (Git git = new Git(createContext().open())) { + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getFullMessage()).isEqualTo("test commit"); + assertThat(lastCommit.getAuthorIdent().getName()).isEqualTo("Dirk Gently"); + assertThat(newRef).isEqualTo(lastCommit.toObjectId().name()); + } + + assertThat(outputStream).hasToString("new content"); + } + @Test public void shouldCreateSecondCommits() throws IOException, GitAPIException { createCommit("new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", new ByteArrayOutputStream()); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactoryTest.java index 3c6b6922af..0e3b381a10 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactoryTest.java @@ -60,7 +60,7 @@ public class GitPostReceiveRepositoryHookEventFactoryTest extends AbstractGitCom when(hookContext.getBranchProvider().getCreatedOrModified()).thenReturn(branches); when(hookContext.getTagProvider().getCreatedTags()).thenReturn(tags); - RepositoryHookEvent event = eventFactory.createEvent( + RepositoryHookEvent event = eventFactory.createPostReceiveEvent( createContext(), branches, tags, diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagCommandTest.java index 4a6a2df4f2..35d5cf2bde 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagCommandTest.java @@ -22,8 +22,9 @@ import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Signers; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.junit.After; @@ -81,7 +82,7 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase { @Before public void setSigner() { - GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); + Signers.set(GpgConfig.GpgFormat.OPENPGP, new GitTestHelper.SimpleGpgSigner()); } @Before diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitUnbundleCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitUnbundleCommandTest.java index d98268b0e6..d61b02204e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitUnbundleCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitUnbundleCommandTest.java @@ -49,7 +49,7 @@ public class GitUnbundleCommandTest extends AbstractGitCommandTestBase { @Test public void shouldUnbundleRepositoryFiles() throws IOException { RepositoryHookEvent event = new RepositoryHookEvent(null, repository, RepositoryHookType.POST_RECEIVE); - when(eventFactory.createEvent(eq(createContext()), any(), any(), any())).thenReturn(event); + when(eventFactory.createPostReceiveEvent(eq(createContext()), any(), any(), any())).thenReturn(event); AtomicReference receivedEvent = new AtomicReference<>(); diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip index b97f519684..dc383f79d2 100644 Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-lfs-test.zip differ diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-move-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-move-test.zip index 15ddfe1c13..ec020a6677 100644 Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-move-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-move-test.zip differ