mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-12-24 01:09:48 +01:00
Fast modifications inside git repositories
With this change, most modifications of git repositories (like inserting, deleting and updating files and merging branches) do no longer work inside clones held in temporary working directories but are done directly inside the bare git repository data. This resolves in a massive performance boost for the editor plugin and pull requests, especially in larger repositories. Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com> Committed-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
This commit is contained in:
2
gradle/changelog/fast_git_modifications.yaml
Normal file
2
gradle/changelog/fast_git_modifications.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Performance improvements for git modifications
|
||||
2
gradle/changelog/jgit7.yaml
Normal file
2
gradle/changelog/jgit7.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: changed
|
||||
description: Upgrade JGit to 7.1.0.202411261347-r
|
||||
@@ -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
|
||||
|
||||
@@ -49,7 +49,6 @@ public class GitHeadModifier {
|
||||
*
|
||||
* @param repository repository to modify
|
||||
* @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) {
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -78,7 +78,7 @@ class AbstractGitCommand {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
Repository open() throws IOException {
|
||||
Repository open() {
|
||||
return context.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<ReceiveCommand> receiveCommands = asList(createReceiveCommand());
|
||||
return x -> {
|
||||
|
||||
@@ -53,7 +53,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Branch> getBranches() throws IOException {
|
||||
public List<Branch> 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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
try {
|
||||
gitRepository = GitUtil.open(directory);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(repository, "could not open git repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
return gitRepository;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Tag> newTags;
|
||||
private final GitLazyChangesetResolver changesetResolver;
|
||||
private final Supplier<Iterable<RevCommit>> changesetResolver;
|
||||
private final List<String> newBranches;
|
||||
|
||||
GitImportHookContextProvider(GitChangesetConverter converter,
|
||||
List<String> newBranches,
|
||||
List<Tag> newTags,
|
||||
GitLazyChangesetResolver changesetResolver) {
|
||||
Supplier<Iterable<RevCommit>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Iterable<RevCommit>> {
|
||||
class GitLazyChangesetResolver implements Supplier<Iterable<RevCommit>> {
|
||||
private final Repository repository;
|
||||
private final Git git;
|
||||
|
||||
@@ -37,7 +37,7 @@ class GitLazyChangesetResolver implements Callable<Iterable<RevCommit>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<RevCommit> call() {
|
||||
public Iterable<RevCommit> get() {
|
||||
try {
|
||||
return git.log().all().call();
|
||||
} catch (IOException | GitAPIException e) {
|
||||
|
||||
@@ -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,8 +39,7 @@ 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 =
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Factory {
|
||||
|
||||
@@ -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<MergeStrategy> 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
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@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);
|
||||
List<RevCommit> 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);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.getStatus().isSuccessful()) {
|
||||
return fastForwardTargetBranch(branchToMerge, targetBranch, result);
|
||||
private List<RevCommit> computeCommits() throws IOException {
|
||||
List<RevCommit> 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 {
|
||||
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")));
|
||||
log.trace("skip {} because it has more than one parent", commit);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
return cherryPickList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MergeCommandResult> {
|
||||
|
||||
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<RevCommit> 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());
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Lock> 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());
|
||||
}
|
||||
|
||||
private class ModifyWorker extends GitCloneWorker<String> implements ModifyWorkerHelper {
|
||||
InPlaceWorker inPlaceWorker = new InPlaceWorker(repository);
|
||||
|
||||
private final File workDir;
|
||||
private final ModifyCommandRequest request;
|
||||
|
||||
ModifyWorker(Git clone, ModifyCommandRequest request) {
|
||||
super(clone, context, repository);
|
||||
this.workDir = clone.getRepository().getWorkTree();
|
||||
this.request = request;
|
||||
try (RevWalk revWalk = new RevWalk(repository)) {
|
||||
parentCommit = parentCommitId == null ? null : revWalk.parseCommit(parentCommitId);
|
||||
}
|
||||
|
||||
@Override
|
||||
String run() throws IOException {
|
||||
getClone().getRepository().getFullBranch();
|
||||
|
||||
boolean initialCommit = getClone().getRepository().getRefDatabase().getRefs().isEmpty();
|
||||
|
||||
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> revCommit = doCommit(request.getCommitMessage(), request.getAuthor(), request.isSign());
|
||||
|
||||
if (initialCommit) {
|
||||
handleBranchForInitialCommit();
|
||||
r.execute(inPlaceWorker);
|
||||
}
|
||||
|
||||
push();
|
||||
return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name();
|
||||
TreeHelper treeHelper = new TreeHelper(repository);
|
||||
if (parentCommitId != null) {
|
||||
treeHelper.initialize(parentCommitId, inPlaceWorker.changes);
|
||||
}
|
||||
|
||||
private void handleBranchForInitialCommit() {
|
||||
String branch = StringUtils.isNotBlank(request.getBranch()) ? request.getBranch() : context.getGlobalConfig().getDefaultBranch();
|
||||
if (StringUtils.isNotBlank(branch)) {
|
||||
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 static String removeStartingSlash(String toBeCreated) {
|
||||
return toBeCreated.startsWith("/") ? toBeCreated.substring(1) : toBeCreated;
|
||||
}
|
||||
|
||||
private class TreeHelper {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void initialize(ObjectId parentCommitId, Collection<TreeChange> changes) throws IOException {
|
||||
ObjectId parentTreeId = getTreeId(parentCommitId);
|
||||
try (TreeWalk treeWalk = new TreeWalk(repository)) {
|
||||
|
||||
treeWalk.addTree(parentTreeId);
|
||||
treeWalk.setRecursive(true);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ObjectId getTreeId(ObjectId commitId) throws IOException {
|
||||
try (RevWalk revWalk = new RevWalk(repository)) {
|
||||
RevCommit commit = revWalk.parseCommit(commitId);
|
||||
return commit.getTree().getId();
|
||||
}
|
||||
}
|
||||
|
||||
void updateTreeWithNewFile(String filePath, ObjectId blobId) {
|
||||
if (filePath.startsWith("/")) {
|
||||
filePath = filePath.substring(1);
|
||||
}
|
||||
try {
|
||||
createBranchIfNotThere(branch);
|
||||
} catch (GitAPIException | IOException e) {
|
||||
throw new InternalRepositoryException(repository, "could not create default branch for initial commit", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createBranchIfNotThere(String branch) throws IOException, GitAPIException {
|
||||
if (!branch.equals(getClone().getRepository().getBranch())) {
|
||||
getClone().checkout().setName(branch).setCreateBranch(true).call();
|
||||
setBranchInConfig(branch);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
ObjectId flush() throws IOException {
|
||||
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);
|
||||
builder.finish();
|
||||
} catch (DirCacheNameConflictException e) {
|
||||
throw alreadyExists(entity("File", e.getPath1()).in(GitModifyCommand.this.repository));
|
||||
}
|
||||
} finally {
|
||||
REGISTER_LOCKS.get(targetFile).unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMovedFileToScm(String path, Path targetPath) {
|
||||
try {
|
||||
addFileToGit(path);
|
||||
} catch (GitAPIException e) {
|
||||
throwInternalRepositoryException("could not add file to index", e);
|
||||
}
|
||||
}
|
||||
|
||||
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<TreeChange> changes = new ArrayList<>();
|
||||
private final Repository repository;
|
||||
private final Map<String, AttributesNode> 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<Move> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> branches,
|
||||
List<Tag> tags,
|
||||
GitLazyChangesetResolver changesetResolver
|
||||
) throws IOException {
|
||||
Supplier<Iterable<RevCommit>> 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<String> branches,
|
||||
List<Tag> tags,
|
||||
Supplier<Iterable<RevCommit>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -73,11 +73,11 @@ public class GitUnbundleCommand extends AbstractGitCommand implements UnbundleCo
|
||||
List<String> branches = extractBranches(git);
|
||||
List<Tag> 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",
|
||||
|
||||
@@ -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<String> 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<ObjectId, ObjectId, ObjectId[]> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> branches = getBranchesFromFetchResult(result);
|
||||
List<Tag> tags = getTagsFromFetchResult(result);
|
||||
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git);
|
||||
event = new PostReceiveRepositoryHookEvent(WrappedRepositoryHookEvent.wrap(eventFactory.createEvent(context, branches, tags, changesetResolver)));
|
||||
} catch (IOException e) {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(context.getRepository()).build(),
|
||||
"Could not fire post receive repository hook event after fetch",
|
||||
e
|
||||
);
|
||||
}
|
||||
event = new PostReceiveRepositoryHookEvent(WrappedRepositoryHookEvent.wrap(eventFactory.createPostReceiveEvent(context, branches, tags, changesetResolver)));
|
||||
eventBus.post(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<RevCommit> commits = changesetResolver.call();
|
||||
Iterable<RevCommit> 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<RevCommit> allCommits = changesetResolver.call();
|
||||
Iterable<RevCommit> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<RevCommit> 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<Added> 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<RevCommit> 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<RevCommit> 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<Git> 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, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkingCopyFactory workingCopyFactory, String initialBranch) {
|
||||
Function<Git, W> interceptedWorkerSupplier = git -> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<RepositoryHookEvent> receivedEvent = new AtomicReference<>();
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user