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:
Rene Pfeuffer
2025-01-07 11:06:53 +01:00
parent e615bc32ad
commit 8422c3bc44
42 changed files with 1201 additions and 611 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Performance improvements for git modifications

View File

@@ -0,0 +1,2 @@
- type: changed
description: Upgrade JGit to 7.1.0.202411261347-r

View File

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

View File

@@ -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,7 +64,7 @@ public class GitHeadModifier {
}
private String resolve(org.eclipse.jgit.lib.Repository gitRepository) throws IOException {
Ref ref = gitRepository.getRefDatabase().getRef(Constants.HEAD);
Ref ref = gitRepository.getRefDatabase().findRef(Constants.HEAD);
if (ref.isSymbolic()) {
ref = ref.getTarget();
}

View File

@@ -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
*/

View File

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

View File

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

View File

@@ -78,7 +78,7 @@ class AbstractGitCommand {
this.context = context;
}
Repository open() throws IOException {
Repository open() {
return context.open();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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