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' 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 { dependencies {
// required by scm-it // required by scm-it

View File

@@ -48,11 +48,10 @@ public class GitHeadModifier {
* repositories head points already to the given branch. * repositories head points already to the given branch.
* *
* @param repository repository to modify * @param repository repository to modify
* @param newHead branch which should be the new head of the repository * @param newHead branch which should be the new head of the repository
*
* @return {@code true} if the head has changed * @return {@code true} if the head has changed
*/ */
public boolean ensure(Repository repository, String newHead) { public boolean ensure(Repository repository, String newHead) {
try (org.eclipse.jgit.lib.Repository gitRepository = open(repository)) { try (org.eclipse.jgit.lib.Repository gitRepository = open(repository)) {
String currentHead = resolve(gitRepository); String currentHead = resolve(gitRepository);
if (!Objects.equals(currentHead, newHead)) { if (!Objects.equals(currentHead, newHead)) {
@@ -65,8 +64,8 @@ public class GitHeadModifier {
} }
private String resolve(org.eclipse.jgit.lib.Repository gitRepository) throws IOException { 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() ) { if (ref.isSymbolic()) {
ref = ref.getTarget(); ref = ref.getTarget();
} }
return GitUtil.getBranch(ref); return GitUtil.getBranch(ref);

View File

@@ -239,9 +239,7 @@ public final class GitUtil {
String branchName) String branchName)
throws IOException { throws IOException {
Ref ref = null; Ref ref = null;
if (!branchName.startsWith(REF_HEAD)) { branchName = getRevString(branchName);
branchName = PREFIX_HEADS.concat(branchName);
}
checkBranchName(repo, branchName); checkBranchName(repo, branchName);
@@ -258,6 +256,13 @@ public final class GitUtil {
return ref; return ref;
} }
public static String getRevString(String branchName) {
if (!branchName.startsWith(REF_HEAD)) {
return PREFIX_HEADS.concat(branchName);
}
return branchName;
}
/** /**
* @since 2.5.0 * @since 2.5.0
*/ */

View File

@@ -18,16 +18,18 @@ package sonia.scm.repository;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.eclipse.jgit.api.errors.CanceledException; 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.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent; 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 org.eclipse.jgit.transport.CredentialsProvider;
import sonia.scm.security.GPG; 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; private final GPG gpg;
@@ -37,17 +39,13 @@ public class ScmGpgSigner extends GpgSigner {
} }
@Override @Override
public void sign(CommitBuilder commitBuilder, String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { public GpgSignature sign(Repository repository, GpgConfig gpgConfig, byte[] bytes, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) throws CanceledException, IOException, UnsupportedSigningFormatException {
try { final byte[] signature = this.gpg.getPrivateKey().sign(bytes);
final byte[] signature = this.gpg.getPrivateKey().sign(commitBuilder.build()); return new GpgSignature(signature);
commitBuilder.setGpgSignature(new GpgSignature(signature));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
} }
@Override @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; return true;
} }
} }

View File

@@ -19,7 +19,8 @@ package sonia.scm.repository;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.servlet.ServletContextEvent; import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener; 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; import sonia.scm.plugin.Extension;
@Extension @Extension
@@ -34,7 +35,7 @@ public class ScmGpgSignerInitializer implements ServletContextListener {
@Override @Override
public void contextInitialized(ServletContextEvent servletContextEvent) { public void contextInitialized(ServletContextEvent servletContextEvent) {
GpgSigner.setDefault(scmGpgSigner); Signers.set(GpgConfig.GpgFormat.OPENPGP, scmGpgSigner);
} }
@Override @Override

View File

@@ -78,7 +78,7 @@ class AbstractGitCommand {
this.context = context; this.context = context;
} }
Repository open() throws IOException { Repository open() {
return context.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); log.debug("got exception for invalid branch name {}", request.getNewBranch(), e);
doThrow().violation("Invalid branch name", "name").when(true); doThrow().violation("Invalid branch name", "name").when(true);
return null; return null;
} catch (GitAPIException | IOException ex) { } catch (GitAPIException ex) {
throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), 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)); eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
} catch (CannotDeleteCurrentBranchException e) { } catch (CannotDeleteCurrentBranchException e) {
throw new CannotDeleteDefaultBranchException(context.getRepository(), branchName); 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)); 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 @Override
public HookChangesetProvider getChangesetProvider() { public HookChangesetProvider getChangesetProvider() {
Repository gitRepo; Repository gitRepo = context.open();
try {
gitRepo = context.open();
} catch (IOException e) {
throw new InternalRepositoryException(repository, "failed to open repository for post receive hook after internal change", e);
}
Collection<ReceiveCommand> receiveCommands = asList(createReceiveCommand()); Collection<ReceiveCommand> receiveCommands = asList(createReceiveCommand());
return x -> { return x -> {

View File

@@ -53,7 +53,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
} }
@Override @Override
public List<Branch> getBranches() throws IOException { public List<Branch> getBranches() {
Git git = createGit(); Git git = createGit();
String defaultBranchName = determineDefaultBranchName(git); String defaultBranchName = determineDefaultBranchName(git);
@@ -72,7 +72,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
} }
@VisibleForTesting @VisibleForTesting
Git createGit() throws IOException { Git createGit() {
return new Git(open()); 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.GitConfig;
import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryProvider; 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) if (gitRepository == null)
{ {
logger.trace("open git repository {}", directory); logger.trace("open git repository {}", directory);
gitRepository = GitUtil.open(directory); try {
gitRepository = GitUtil.open(directory);
} catch (IOException e) {
throw new InternalRepositoryException(repository, "could not open git repository", e);
}
} }
return gitRepository; return gitRepository;

View File

@@ -16,38 +16,41 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git; import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.api.MergeResult;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeCommandResult;
import java.io.IOException; @Slf4j
import java.util.Collections; 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(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
this.request = request;
GitFastForwardIfPossible(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
super(clone, request, context, repository); this.fallbackMerge = new GitMergeCommit(request, context, repositoryManager, eventFactory);
fallbackMerge = new GitMergeCommit(clone, request, context, repository); this.commitHelper = new CommitHelper(context, repositoryManager, eventFactory);
this.repository = context.getRepository();
} }
@Override MergeCommandResult run() {
MergeCommandResult run() throws IOException { log.trace("try to fast forward branch {} onto {} in repository {}", request.getBranchToMerge(), request.getTargetBranch(), repository);
MergeResult fastForwardResult = mergeWithFastForwardOnlyMode(); ObjectId sourceRevision = mergeHelper.getRevisionToMerge();
if (fastForwardResult.getMergeStatus().isSuccessful()) { ObjectId targetRevision = mergeHelper.getTargetRevision();
push();
return createSuccessResult(fastForwardResult.getNewHead().name()); 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 { } else {
log.trace("fast forward is not possible, fallback to merge");
return fallbackMerge.run(); 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; package sonia.scm.repository.spi;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.eclipse.jgit.revwalk.RevCommit;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.Tag; import sonia.scm.repository.Tag;
import sonia.scm.repository.api.HookBranchProvider; import sonia.scm.repository.api.HookBranchProvider;
@@ -27,17 +28,19 @@ import sonia.scm.repository.api.HookTagProvider;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
class GitImportHookContextProvider extends HookContextProvider { class GitImportHookContextProvider extends HookContextProvider {
private final GitChangesetConverter converter; private final GitChangesetConverter converter;
private final List<Tag> newTags; private final List<Tag> newTags;
private final GitLazyChangesetResolver changesetResolver; private final Supplier<Iterable<RevCommit>> changesetResolver;
private final List<String> newBranches; private final List<String> newBranches;
GitImportHookContextProvider(GitChangesetConverter converter, GitImportHookContextProvider(GitChangesetConverter converter,
List<String> newBranches, List<String> newBranches,
List<Tag> newTags, List<Tag> newTags,
GitLazyChangesetResolver changesetResolver) { Supplier<Iterable<RevCommit>> changesetResolver) {
this.converter = converter; this.converter = converter;
this.newTags = newTags; this.newTags = newTags;
this.changesetResolver = changesetResolver; this.changesetResolver = changesetResolver;
@@ -81,7 +84,7 @@ class GitImportHookContextProvider extends HookContextProvider {
@Override @Override
public HookChangesetProvider getChangesetProvider() { public HookChangesetProvider getChangesetProvider() {
GitConvertingChangesetIterable changesets = new GitConvertingChangesetIterable(changesetResolver.call(), converter); GitConvertingChangesetIterable changesets = new GitConvertingChangesetIterable(changesetResolver.get(), converter);
return r -> new HookChangesetResponse(changesets); return r -> new HookChangesetResponse(changesets);
} }
} }

View File

@@ -23,11 +23,11 @@ import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.Callable; import java.util.function.Supplier;
import static sonia.scm.ContextEntry.ContextBuilder.entity; 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 Repository repository;
private final Git git; private final Git git;
@@ -37,7 +37,7 @@ class GitLazyChangesetResolver implements Callable<Iterable<RevCommit>> {
} }
@Override @Override
public Iterable<RevCommit> call() { public Iterable<RevCommit> get() {
try { try {
return git.log().all().call(); return git.log().all().call();
} catch (IOException | GitAPIException e) { } catch (IOException | GitAPIException e) {

View File

@@ -31,7 +31,6 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import java.io.IOException; import java.io.IOException;
@@ -40,10 +39,9 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound; 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 = private static final Logger logger =
LoggerFactory.getLogger(GitLogCommand.class); LoggerFactory.getLogger(GitLogCommand.class);
public static final String REVISION = "Revision"; public static final String REVISION = "Revision";
@@ -51,20 +49,16 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
@Inject @Inject
GitLogCommand(@Assisted GitContext context, GitChangesetConverterFactory converterFactory) GitLogCommand(@Assisted GitContext context, GitChangesetConverterFactory converterFactory) {
{
super(context); super(context);
this.converterFactory = converterFactory; this.converterFactory = converterFactory;
} }
@Override @Override
@SuppressWarnings("java:S2093") @SuppressWarnings("java:S2093")
public Changeset getChangeset(String revision, LogCommandRequest request) public Changeset getChangeset(String revision, LogCommandRequest request) {
{ if (logger.isDebugEnabled()) {
if (logger.isDebugEnabled())
{
logger.debug("fetch changeset {}", revision); logger.debug("fetch changeset {}", revision);
} }
@@ -73,18 +67,15 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
GitChangesetConverter converter = null; GitChangesetConverter converter = null;
RevWalk revWalk = null; RevWalk revWalk = null;
try try {
{
gr = open(); gr = open();
if (!gr.getAllRefs().isEmpty()) if (!gr.getAllRefs().isEmpty()) {
{
revWalk = new RevWalk(gr); revWalk = new RevWalk(gr);
ObjectId id = GitUtil.getRevisionId(gr, revision); ObjectId id = GitUtil.getRevisionId(gr, revision);
RevCommit commit = revWalk.parseCommit(id); RevCommit commit = revWalk.parseCommit(id);
if (commit != null) if (commit != null) {
{
converter = converterFactory.create(gr, revWalk); converter = converterFactory.create(gr, revWalk);
if (isBranchRequested(request)) { if (isBranchRequested(request)) {
@@ -98,23 +89,15 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
} else { } else {
changeset = converter.createChangeset(commit); changeset = converter.createChangeset(commit);
} }
} } else if (logger.isWarnEnabled()) {
else if (logger.isWarnEnabled())
{
logger.warn("could not find revision {}", revision); logger.warn("could not find revision {}", revision);
} }
} }
} } catch (IOException ex) {
catch (IOException ex)
{
logger.error("could not open repository: " + repository.getNamespaceAndName(), ex); logger.error("could not open repository: " + repository.getNamespaceAndName(), ex);
} } catch (NullPointerException e) {
catch (NullPointerException e)
{
throw notFound(entity(REVISION, revision).in(this.repository)); throw notFound(entity(REVISION, revision).in(this.repository));
} } finally {
finally
{
IOUtil.close(converter); IOUtil.close(converter);
GitUtil.release(revWalk); GitUtil.release(revWalk);
} }
@@ -138,14 +121,10 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
@Override @Override
@SuppressWarnings("java:S2093") @SuppressWarnings("java:S2093")
public ChangesetPagingResult getChangesets(LogCommandRequest request) { public ChangesetPagingResult getChangesets(LogCommandRequest request) {
try { if (Strings.isNullOrEmpty(request.getBranch())) {
if (Strings.isNullOrEmpty(request.getBranch())) { request.setBranch(context.getConfig().getDefaultBranch());
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);
} }
return new GitLogComputer(this.repository.getId(), open(), converterFactory).compute(request);
} }
public interface Factory { 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.GitRepositoryHandler;
import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.api.MergePreventReason; import sonia.scm.repository.api.MergePreventReason;
@@ -56,6 +57,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
private final GitWorkingCopyFactory workingCopyFactory; private final GitWorkingCopyFactory workingCopyFactory;
private final AttributeAnalyzer attributeAnalyzer; private final AttributeAnalyzer attributeAnalyzer;
private final RepositoryManager repositoryManager;
private final GitRepositoryHookEventFactory eventFactory;
private static final Set<MergeStrategy> STRATEGIES = Set.of( private static final Set<MergeStrategy> STRATEGIES = Set.of(
MergeStrategy.MERGE_COMMIT, MergeStrategy.MERGE_COMMIT,
MergeStrategy.FAST_FORWARD_IF_POSSIBLE, MergeStrategy.FAST_FORWARD_IF_POSSIBLE,
@@ -64,14 +68,24 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
); );
@Inject @Inject
GitMergeCommand(@Assisted GitContext context, GitRepositoryHandler handler, AttributeAnalyzer attributeAnalyzer) { GitMergeCommand(@Assisted GitContext context,
this(context, handler.getWorkingCopyFactory(), attributeAnalyzer); 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); super(context);
this.workingCopyFactory = workingCopyFactory; this.workingCopyFactory = workingCopyFactory;
this.attributeAnalyzer = attributeAnalyzer; this.attributeAnalyzer = attributeAnalyzer;
this.repositoryManager = repositoryManager;
this.eventFactory = eventFactory;
} }
@Override @Override
@@ -85,22 +99,14 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
} }
private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) { private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) {
switch (request.getMergeStrategy()) { return switch (request.getMergeStrategy()) {
case SQUASH: case SQUASH -> new GitMergeWithSquash(request, context, repositoryManager, eventFactory).run();
return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); case FAST_FORWARD_IF_POSSIBLE ->
new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory).run();
case FAST_FORWARD_IF_POSSIBLE: case MERGE_COMMIT -> new GitMergeCommit(request, context, repositoryManager, eventFactory).run();
return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workingCopyFactory, request.getTargetBranch()); case REBASE -> new GitMergeRebase(request, context, repositoryManager, eventFactory).run();
default -> throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy());
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());
}
} }
@Override @Override

View File

@@ -16,39 +16,21 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.api.MergeCommand; import sonia.scm.repository.RepositoryManager;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.revwalk.RevCommit;
import sonia.scm.NoChangesMadeException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeCommandResult;
import java.io.IOException; class GitMergeCommit {
import java.util.Collections;
import java.util.Optional;
import static sonia.scm.repository.spi.GitRevisionExtractor.extractRevisionFromRevCommit; private final MergeCommandRequest request;
private final MergeHelper mergeHelper;
class GitMergeCommit extends GitMergeStrategy { GitMergeCommit(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
this.request = request;
GitMergeCommit(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
super(clone, request, context, repository);
} }
@Override MergeCommandResult run() {
MergeCommandResult run() throws IOException { return mergeHelper.doRecursiveMerge(request, (sourceRevision, targetRevision) -> new ObjectId[]{targetRevision, sourceRevision});
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);
}
} }
} }

View File

@@ -16,73 +16,108 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git; import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.MergeCommand; import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.RebaseResult; import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.slf4j.Logger; import org.eclipse.jgit.lib.PersonIdent;
import org.slf4j.LoggerFactory; 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.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Person;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeCommandResult;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; 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 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) { GitMergeRebase(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
super(clone, request, context, repository);
this.request = request; this.request = request;
this.context = context;
this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
this.commitHelper = new CommitHelper(context, repositoryManager, eventFactory);
this.fastForwardMerge = new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory);
} }
@Override MergeCommandResult run() {
MergeCommandResult run() throws IOException { log.debug("rebase branch {} onto {}", request.getBranchToMerge(), request.getTargetBranch());
RebaseResult result;
String branchToMerge = request.getBranchToMerge(); ObjectId sourceRevision = mergeHelper.getRevisionToMerge();
String targetBranch = request.getTargetBranch(); ObjectId targetRevision = mergeHelper.getTargetRevision();
try { if (mergeHelper.isMergedInto(targetRevision, sourceRevision)) {
checkOutBranch(branchToMerge); log.trace("fast forward is possible; using fast forward merge");
result = return fastForwardMerge.run();
getClone()
.rebase()
.setUpstream(targetBranch)
.call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(getContext().getRepository(), "could not rebase branch " + branchToMerge + " onto " + targetBranch, e);
} }
if (result.getStatus().isSuccessful()) { try {
return fastForwardTargetBranch(branchToMerge, targetBranch, result); List<RevCommit> commits = computeCommits();
} else { Collections.reverse(commits);
logger.info("could not rebase branch {} into {} with rebase status '{}' due to ...", branchToMerge, targetBranch, result.getStatus());
logger.info("... conflicts: {}", result.getConflicts()); for (RevCommit commit : commits) {
logger.info("... failing paths: {}", result.getFailingPaths()); log.trace("rebase {} onto {}", commit, targetRevision);
logger.info("... message: {}", result); ResolveMerger merger = (ResolveMerger) RESOLVE.newMerger(context.open(), true); // The recursive merger is always a RecursiveMerge
return MergeCommandResult.failure(branchToMerge, targetBranch, Optional.ofNullable(result.getConflicts()).orElse(Collections.singletonList("UNKNOWN"))); merger.setBase(commit.getParent(0));
boolean mergeSucceeded = merger.merge(commit, targetRevision);
if (!mergeSucceeded) {
log.trace("could not merge {} into {}", commit, targetRevision);
return MergeCommandResult.failure(request.getBranchToMerge(), request.getTargetBranch(), ofNullable(merger.getUnmergedPaths()).orElse(Collections.singletonList("UNKNOWN")));
}
ObjectId newTreeId = merger.getResultTreeId();
log.trace("create commit for new tree {}", newTreeId);
PersonIdent originalAuthor = commit.getAuthorIdent();
targetRevision = commitHelper.createCommit(
newTreeId,
new Person(originalAuthor.getName(), originalAuthor.getEmailAddress()),
request.getAuthor(),
commit.getFullMessage(),
request.isSign(),
targetRevision
);
log.trace("created {}", targetRevision);
}
log.trace("update branch {} to new revision {}", request.getTargetBranch(), targetRevision);
commitHelper.updateBranch(request.getTargetBranch(), targetRevision, mergeHelper.getTargetRevision());
return MergeCommandResult.success(targetRevision.name(), mergeHelper.getRevisionToMerge().name(), targetRevision.name());
} catch (IOException | CanceledException | UnsupportedSigningFormatException e) {
throw new InternalRepositoryException(context.getRepository(), "could not rebase branch " + request.getBranchToMerge() + " onto " + request.getTargetBranch(), e);
} }
} }
private MergeCommandResult fastForwardTargetBranch(String branchToMerge, String targetBranch, RebaseResult result) throws IOException { private List<RevCommit> computeCommits() throws IOException {
try { List<RevCommit> cherryPickList = new ArrayList<>();
getClone().checkout().setName(targetBranch).call(); try (RevWalk revWalk = new RevWalk(context.open())) {
ObjectId sourceRevision = resolveRevision(branchToMerge); revWalk.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true);
getClone() revWalk.sort(RevSort.COMMIT_TIME_DESC, true);
.merge() revWalk.markUninteresting(revWalk.lookupCommit(mergeHelper.getTargetRevision()));
.setFastForward(MergeCommand.FastForwardMode.FF_ONLY) revWalk.markStart(revWalk.lookupCommit(mergeHelper.getRevisionToMerge()));
.include(branchToMerge, sourceRevision)
.call();
push();
return createSuccessResult(sourceRevision.name());
} catch (GitAPIException e) {
return MergeCommandResult.failure(branchToMerge, targetBranch, result.getConflicts());
}
for (RevCommit commit : revWalk) {
if (commit.getParentCount() <= 1) {
log.trace("add {} to cherry pick list", commit);
cherryPickList.add(commit);
} else {
log.trace("skip {} because it has more than one parent", commit);
}
}
}
return cherryPickList;
} }
} }

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; package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.api.MergeCommand; import sonia.scm.repository.RepositoryManager;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.revwalk.RevCommit;
import sonia.scm.NoChangesMadeException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.MergeCommandResult; 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(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
this.request = request;
GitMergeWithSquash(Git clone, MergeCommandRequest request, GitContext context, Repository repository) { this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
super(clone, request, context, repository);
} }
@Override MergeCommandResult run() {
MergeCommandResult run() throws IOException { return mergeHelper.doRecursiveMerge(request, (sourceRevision, targetRevision) -> new ObjectId[]{targetRevision});
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);
}
} }
} }

View File

@@ -16,198 +16,194 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import com.google.common.util.concurrent.Striped;
import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.Assisted;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.apache.commons.lang.StringUtils; import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.attributes.AttributesNode;
import org.eclipse.jgit.attributes.FilterCommandRegistry; import org.eclipse.jgit.attributes.AttributesRule;
import org.eclipse.jgit.dircache.DirCache; 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.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.ConcurrentModificationException;
import sonia.scm.ContextEntry;
import sonia.scm.NoChangesMadeException; import sonia.scm.NoChangesMadeException;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.lfs.LfsBlobStoreFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.util.ArrayList;
import java.util.Optional; import java.util.Arrays;
import java.util.concurrent.locks.Lock; 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 { public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand {
private static final Striped<Lock> REGISTER_LOCKS = Striped.lock(5); private final RepositoryManager repositoryManager;
private final GitRepositoryHookEventFactory eventFactory;
private final GitWorkingCopyFactory workingCopyFactory;
private final LfsBlobStoreFactory lfsBlobStoreFactory; private final LfsBlobStoreFactory lfsBlobStoreFactory;
private final GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider;
private RevCommit parentCommit;
@Inject @Inject
GitModifyCommand(@Assisted GitContext context, GitRepositoryHandler repositoryHandler, LfsBlobStoreFactory lfsBlobStoreFactory, GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider) { GitModifyCommand(@Assisted GitContext context, LfsBlobStoreFactory lfsBlobStoreFactory, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
this(context, repositoryHandler.getWorkingCopyFactory(), lfsBlobStoreFactory, gitRepositoryConfigStoreProvider); super(context);
this.repositoryManager = repositoryManager;
this.eventFactory = eventFactory;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
} }
GitModifyCommand(@Assisted GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory, GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider) { private interface TreeChange {
super(context); boolean keepOriginalEntry(String path, ObjectId blob);
this.workingCopyFactory = workingCopyFactory;
this.lfsBlobStoreFactory = lfsBlobStoreFactory; default void finish(TreeHelper treeHelper) {
this.gitRepositoryConfigStoreProvider = gitRepositoryConfigStoreProvider; }
} }
@Override @Override
public String execute(ModifyCommandRequest request) { public String execute(ModifyCommandRequest request) {
return inClone(clone -> new ModifyWorker(clone, request), workingCopyFactory, request.getBranch()); try {
org.eclipse.jgit.lib.Repository repository = context.open();
CommitHelper commitHelper = new CommitHelper(context, repositoryManager, eventFactory);
String branchToChange = request.getBranch() == null ? context.getGlobalConfig().getDefaultBranch() : request.getBranch();
ObjectId parentCommitId = repository.resolve(GitUtil.getRevString(branchToChange));
if (parentCommitId == null && request.getBranch() != null && repository.resolve("HEAD") != null) {
throw notFound(entity("Branch", branchToChange).in(this.repository));
}
if (request.getExpectedRevision() != null && !parentCommitId.name().equals(request.getExpectedRevision())) {
throw new ConcurrentModificationException(entity("Branch", branchToChange).in(this.repository).build());
}
InPlaceWorker inPlaceWorker = new InPlaceWorker(repository);
try (RevWalk revWalk = new RevWalk(repository)) {
parentCommit = parentCommitId == null ? null : revWalk.parseCommit(parentCommitId);
}
for (ModifyCommandRequest.PartialRequest r : request.getRequests()) {
r.execute(inPlaceWorker);
}
TreeHelper treeHelper = new TreeHelper(repository);
if (parentCommitId != null) {
treeHelper.initialize(parentCommitId, inPlaceWorker.changes);
}
inPlaceWorker.finish(treeHelper);
ObjectId treeId = treeHelper.flush();
if (parentCommitId != null) {
if (parentCommit.getTree().equals(treeId)) {
throw new NoChangesMadeException(GitModifyCommand.this.repository, branchToChange);
}
}
ObjectId commitId = commitHelper.createCommit(
treeId,
request.getAuthor(),
request.getAuthor(),
request.getCommitMessage(),
request.isSign(),
parentCommitId == null ? new ObjectId[0] : new ObjectId[]{parentCommitId}
);
commitHelper.updateBranch(branchToChange, commitId, parentCommitId);
return commitId.name();
} catch (IOException | CanceledException | UnsupportedSigningFormatException e) {
throw new InternalRepositoryException(repository, "Error during modification", e);
}
} }
private class ModifyWorker extends GitCloneWorker<String> implements ModifyWorkerHelper { private static String removeStartingSlash(String toBeCreated) {
return toBeCreated.startsWith("/") ? toBeCreated.substring(1) : toBeCreated;
}
private final File workDir; private class TreeHelper {
private final ModifyCommandRequest request;
ModifyWorker(Git clone, ModifyCommandRequest request) { private final org.eclipse.jgit.lib.Repository repository;
super(clone, context, repository); private final DirCacheBuilder builder;
this.workDir = clone.getRepository().getWorkTree(); private final ObjectInserter inserter;
this.request = request; private final DirCache dirCache = DirCache.newInCore();
TreeHelper(Repository repository) {
this.repository = repository;
this.inserter = repository.newObjectInserter();
this.builder = dirCache.builder();
} }
@Override private void initialize(ObjectId parentCommitId, Collection<TreeChange> changes) throws IOException {
String run() throws IOException { ObjectId parentTreeId = getTreeId(parentCommitId);
getClone().getRepository().getFullBranch(); try (TreeWalk treeWalk = new TreeWalk(repository)) {
boolean initialCommit = getClone().getRepository().getRefDatabase().getRefs().isEmpty(); treeWalk.addTree(parentTreeId);
treeWalk.setRecursive(true);
if (!StringUtils.isEmpty(request.getExpectedRevision()) while (treeWalk.next()) {
&& !request.getExpectedRevision().equals(getCurrentObjectId().getName())) { String path = treeWalk.getPathString();
throw new ConcurrentModificationException(ContextEntry.ContextBuilder.entity("Branch", request.getBranch() == null ? "default" : request.getBranch()).in(repository).build()); if (changes.stream().allMatch(c -> c.keepOriginalEntry(path, treeWalk.getObjectId(0)))) {
} DirCacheEntry entry = new DirCacheEntry(path);
for (ModifyCommandRequest.PartialRequest r : request.getRequests()) { entry.setObjectId(treeWalk.getObjectId(0));
r.execute(this); entry.setFileMode(treeWalk.getFileMode(0));
} builder.add(entry);
failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())); }
Optional<RevCommit> revCommit = doCommit(request.getCommitMessage(), request.getAuthor(), request.isSign());
if (initialCommit) {
handleBranchForInitialCommit();
}
push();
return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name();
}
private void handleBranchForInitialCommit() {
String branch = StringUtils.isNotBlank(request.getBranch()) ? request.getBranch() : context.getGlobalConfig().getDefaultBranch();
if (StringUtils.isNotBlank(branch)) {
try {
createBranchIfNotThere(branch);
} catch (GitAPIException | IOException e) {
throw new InternalRepositoryException(repository, "could not create default branch for initial commit", e);
} }
} }
} }
private void createBranchIfNotThere(String branch) throws IOException, GitAPIException { ObjectId getTreeId(ObjectId commitId) throws IOException {
if (!branch.equals(getClone().getRepository().getBranch())) { try (RevWalk revWalk = new RevWalk(repository)) {
getClone().checkout().setName(branch).setCreateBranch(true).call(); RevCommit commit = revWalk.parseCommit(commitId);
setBranchInConfig(branch); return commit.getTree().getId();
} }
} }
private void setBranchInConfig(String branch) { void updateTreeWithNewFile(String filePath, ObjectId blobId) {
gitRepositoryConfigStoreProvider.setDefaultBranch(repository, branch); if (filePath.startsWith("/")) {
} filePath = filePath.substring(1);
}
@Override
public void addFileToScm(String name, Path file) {
addToGitWithLfsSupport(name, file);
}
private void addToGitWithLfsSupport(String path, Path targetFile) {
REGISTER_LOCKS.get(targetFile).lock();
try { try {
LfsBlobStoreCleanFilterFactory cleanFilterFactory = new LfsBlobStoreCleanFilterFactory(lfsBlobStoreFactory, repository, targetFile); DirCacheEntry newEntry = new DirCacheEntry(filePath);
newEntry.setObjectId(blobId);
String registerKey = "git-lfs clean -- '" + path + "'"; newEntry.setFileMode(FileMode.REGULAR_FILE);
LOG.debug("register lfs filter command factory for command '{}'", registerKey); builder.add(newEntry);
FilterCommandRegistry.register(registerKey, cleanFilterFactory::createFilter); } catch (InvalidPathException e) {
try { doThrow().violation("Path", filePath).when(true);
addFileToGit(path);
} catch (GitAPIException e) {
throwInternalRepositoryException("could not add file to index", e);
} finally {
LOG.debug("unregister lfs filter command factory for command \"{}\"", registerKey);
FilterCommandRegistry.unregister(registerKey);
}
} finally {
REGISTER_LOCKS.get(targetFile).unlock();
} }
} }
@Override ObjectId flush() throws IOException {
public void addMovedFileToScm(String path, Path targetPath) {
try { try {
addFileToGit(path); builder.finish();
} catch (GitAPIException e) { } catch (DirCacheNameConflictException e) {
throwInternalRepositoryException("could not add file to index", e); throw alreadyExists(entity("File", e.getPath1()).in(GitModifyCommand.this.repository));
} }
} ObjectId newTreeId = dirCache.writeTree(inserter);
inserter.flush();
private void addFileToGit(String toBeCreated) throws GitAPIException { return newTreeId;
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);
} }
} }
@@ -215,4 +211,245 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
ModifyCommand create(GitContext context); 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; package sonia.scm.repository.spi;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.eclipse.jgit.revwalk.RevCommit;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.RepositoryHookEvent; 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.HookContext;
import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookContextFactory;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import static sonia.scm.repository.RepositoryHookType.POST_RECEIVE; import static sonia.scm.repository.RepositoryHookType.POST_RECEIVE;
import static sonia.scm.repository.RepositoryHookType.PRE_RECEIVE;
class GitRepositoryHookEventFactory { class GitRepositoryHookEventFactory {
@@ -40,14 +42,23 @@ class GitRepositoryHookEventFactory {
this.changesetConverterFactory = changesetConverterFactory; this.changesetConverterFactory = changesetConverterFactory;
} }
RepositoryHookEvent createEvent(GitContext gitContext, RepositoryHookEvent createPostReceiveEvent(GitContext gitContext,
List<String> branches, List<String> branches,
List<Tag> tags, List<Tag> tags,
GitLazyChangesetResolver changesetResolver Supplier<Iterable<RevCommit>> changesetResolver) {
) throws IOException {
GitChangesetConverter converter = changesetConverterFactory.create(gitContext.open()); GitChangesetConverter converter = changesetConverterFactory.create(gitContext.open());
GitImportHookContextProvider contextProvider = new GitImportHookContextProvider(converter, branches, tags, changesetResolver); GitImportHookContextProvider contextProvider = new GitImportHookContextProvider(converter, branches, tags, changesetResolver);
HookContext context = hookContextFactory.createContext(contextProvider, gitContext.getRepository()); HookContext context = hookContextFactory.createContext(contextProvider, gitContext.getRepository());
return new RepositoryHookEvent(context, gitContext.getRepository(), POST_RECEIVE); 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())) .map(tag -> new ReceiveCommand(fromString(tag.getRevision()), zeroId(), REFS_TAGS_PREFIX + tag.getName()))
.forEach(receiveCommands::add); .forEach(receiveCommands::add);
return x -> { return x -> {
Repository gitRepo; Repository gitRepo = context.open();
try {
gitRepo = context.open();
} catch (IOException e) {
throw new InternalRepositoryException(repository, "failed to open repository for post receive hook after internal change", e);
}
GitHookChangesetCollector collector = GitHookChangesetCollector collector =
GitHookChangesetCollector.collectChangesets( GitHookChangesetCollector.collectChangesets(
converterFactory, converterFactory,

View File

@@ -73,11 +73,11 @@ public class GitUnbundleCommand extends AbstractGitCommand implements UnbundleCo
List<String> branches = extractBranches(git); List<String> branches = extractBranches(git);
List<Tag> tags = extractTags(git); List<Tag> tags = extractTags(git);
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), 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) { if (event != null) {
request.getPostEventSink().accept(event); request.getPostEventSink().accept(event);
} }
} catch (IOException | GitAPIException e) { } catch (GitAPIException e) {
throw new ImportFailedException( throw new ImportFailedException(
ContextEntry.ContextBuilder.entity(context.getRepository()).build(), ContextEntry.ContextBuilder.entity(context.getRepository()).build(),
"Could not fire post receive repository hook event after unbundle", "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 jakarta.inject.Inject;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.FetchResult;
import sonia.scm.ContextEntry;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.PostReceiveRepositoryHookEvent; import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.Tag; import sonia.scm.repository.Tag;
import sonia.scm.repository.WrappedRepositoryHookEvent; import sonia.scm.repository.WrappedRepositoryHookEvent;
import sonia.scm.repository.api.ImportFailedException;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -46,18 +43,10 @@ public class PostReceiveRepositoryHookEventFactory {
void fireForFetch(Git git, FetchResult result) { void fireForFetch(Git git, FetchResult result) {
PostReceiveRepositoryHookEvent event; PostReceiveRepositoryHookEvent event;
try { List<String> branches = getBranchesFromFetchResult(result);
List<String> branches = getBranchesFromFetchResult(result); List<Tag> tags = getTagsFromFetchResult(result);
List<Tag> tags = getTagsFromFetchResult(result); GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git);
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git); event = new PostReceiveRepositoryHookEvent(WrappedRepositoryHookEvent.wrap(eventFactory.createPostReceiveEvent(context, branches, tags, changesetResolver)));
event = new PostReceiveRepositoryHookEvent(WrappedRepositoryHookEvent.wrap(eventFactory.createEvent(context, branches, tags, changesetResolver)));
} catch (IOException e) {
throw new ImportFailedException(
ContextEntry.ContextBuilder.entity(context.getRepository()).build(),
"Could not fire post receive repository hook event after fetch",
e
);
}
eventBus.post(event); 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.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
import org.eclipse.jgit.api.Git; 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.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
import org.eclipse.jgit.internal.JGitText; 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.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent; 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.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
@@ -134,21 +138,21 @@ class GitChangesetConverterTest {
private PublicKey publicKey; private PublicKey publicKey;
private PGPKeyPair keyPair; private PGPKeyPair keyPair;
private GpgSigner defaultSigner; private Signer defaultSigner;
@BeforeEach @BeforeEach
void setUpTestingSignerAndCaptureDefault() throws Exception { 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 // we use the same keypair for all tests to speed things up a little bit
if (keyPair == null) { if (keyPair == null) {
keyPair = createKeyPair(); keyPair = createKeyPair();
GpgSigner.setDefault(new TestingGpgSigner(keyPair)); Signers.set(GpgConfig.GpgFormat.OPENPGP, new TestingGpgSigner(keyPair));
} }
} }
@AfterEach @AfterEach
void restoreDefaultSigner() { void restoreDefaultSigner() {
GpgSigner.setDefault(defaultSigner); Signers.set(GpgConfig.GpgFormat.OPENPGP, defaultSigner);
} }
@Test @Test
@@ -242,7 +246,7 @@ class GitChangesetConverterTest {
return new JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, pair, new Date()); 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; private final PGPKeyPair keyPair;
@@ -251,13 +255,7 @@ class GitChangesetConverterTest {
} }
@Override @Override
public boolean canLocateSigningKey(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 {
return true;
}
@Override
public void sign(CommitBuilder commit, String gpgSigningKey,
PersonIdent committer, CredentialsProvider credentialsProvider) {
try { try {
if (keyPair == null) { if (keyPair == null) {
throw new JGitInternalException(JGitText.get().unableToSignCommitNoSecretKey); throw new JGitInternalException(JGitText.get().unableToSignCommitNoSecretKey);
@@ -274,15 +272,18 @@ class GitChangesetConverterTest {
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) { try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) {
signatureGenerator.update(commit.build());
signatureGenerator.generate().encode(out); signatureGenerator.generate().encode(out);
} }
commit.setGpgSignature(new GpgSignature(buffer.toByteArray())); return new GpgSignature(buffer.toByteArray());
} catch (PGPException | IOException e) { } catch (PGPException | IOException e) {
throw new JGitInternalException(e.getMessage(), 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 // register bouncy castle provider on load

View File

@@ -16,11 +16,11 @@
package sonia.scm.repository; package sonia.scm.repository;
import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.lib.GpgConfig;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature; import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.PersonIdent; 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 org.eclipse.jgit.transport.CredentialsProvider;
import sonia.scm.security.GPG; import sonia.scm.security.GPG;
import sonia.scm.security.PrivateKey; import sonia.scm.security.PrivateKey;
@@ -38,20 +38,19 @@ public final class GitTestHelper {
return new GitChangesetConverterFactory(new NoopGPG()); return new GitChangesetConverterFactory(new NoopGPG());
} }
public static class SimpleGpgSigner extends GpgSigner { public static class SimpleGpgSigner implements Signer {
public static byte[] getSignature() { public static byte[] getSignature() {
return "SIGNATURE".getBytes(); return "SIGNATURE".getBytes();
} }
@Override @Override
public void sign(CommitBuilder commitBuilder, String s, PersonIdent personIdent, CredentialsProvider public GpgSignature sign(Repository repository, GpgConfig gpgConfig, byte[] bytes, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) {
credentialsProvider) throws CanceledException { return new GpgSignature(SimpleGpgSigner.getSignature());
commitBuilder.setGpgSignature(new GpgSignature(SimpleGpgSigner.getSignature()));
} }
@Override @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; return true;
} }

View File

@@ -18,12 +18,9 @@ package sonia.scm.repository;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; import org.eclipse.jgit.lib.GpgConfig;
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.PersonIdent; 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.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -79,7 +76,7 @@ class ScmGpgSignerTest {
when(gpg.getPrivateKey()).thenReturn(privateKey); when(gpg.getPrivateKey()).thenReturn(privateKey);
GpgSigner.setDefault(signer); Signers.set(GpgConfig.GpgFormat.OPENPGP, signer);
Path repositoryPath = workdir.resolve("repository"); Path repositoryPath = workdir.resolve("repository");
Git git = Git.init().setDirectory(repositoryPath.toFile()).call(); Git git = Git.init().setDirectory(repositoryPath.toFile()).call();
@@ -103,6 +100,6 @@ class ScmGpgSignerTest {
@Test @Test
void canLocateSigningKey() throws CanceledException { 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) 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; return context;

View File

@@ -34,7 +34,7 @@ public class GitLazyChangesetResolverTest extends AbstractGitCommandTestBase {
@Test @Test
public void shouldResolveChangesets() throws IOException { public void shouldResolveChangesets() throws IOException {
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(repository, Git.wrap(createContext().open())); GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(repository, Git.wrap(createContext().open()));
Iterable<RevCommit> commits = changesetResolver.call(); Iterable<RevCommit> commits = changesetResolver.get();
RevCommit firstCommit = commits.iterator().next(); RevCommit firstCommit = commits.iterator().next();
assertThat(firstCommit.getId().toString()).isEqualTo("commit a8495c0335a13e6e432df90b3727fa91943189a7 1602078219 -----sp"); assertThat(firstCommit.getId().toString()).isEqualTo("commit a8495c0335a13e6e432df90b3727fa91943189a7 1602078219 -----sp");
@@ -46,7 +46,7 @@ public class GitLazyChangesetResolverTest extends AbstractGitCommandTestBase {
public void shouldResolveAllChangesets() throws IOException, GitAPIException { public void shouldResolveAllChangesets() throws IOException, GitAPIException {
Git git = Git.wrap(createContext().open()); Git git = Git.wrap(createContext().open());
GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(repository, git); GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(repository, git);
Iterable<RevCommit> allCommits = changesetResolver.call(); Iterable<RevCommit> allCommits = changesetResolver.get();
int allCommitsCounter = Iterables.size(allCommits); int allCommitsCounter = Iterables.size(allCommits);
int singleBranchCommitsCounter = Iterables.size(git.log().call()); int singleBranchCommitsCounter = Iterables.size(git.log().call());
@@ -57,7 +57,7 @@ public class GitLazyChangesetResolverTest extends AbstractGitCommandTestBase {
public void shouldThrowImportFailedException() { public void shouldThrowImportFailedException() {
Git git = mock(Git.class); Git git = mock(Git.class);
doThrow(ImportFailedException.class).when(git).log(); 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) { private MergeConflictResult computeMergeConflictResult(String branchToMerge, String targetBranch) {
AttributeAnalyzer attributeAnalyzer = mock(AttributeAnalyzer.class); AttributeAnalyzer attributeAnalyzer = mock(AttributeAnalyzer.class);
when(attributeAnalyzer.hasExternalMergeToolConflicts(any(), any())).thenReturn(false); 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 mergeCommandRequest = new MergeCommandRequest();
mergeCommandRequest.setBranchToMerge(branchToMerge); mergeCommandRequest.setBranchToMerge(branchToMerge);
mergeCommandRequest.setTargetBranch(targetBranch); mergeCommandRequest.setTargetBranch(targetBranch);

View File

@@ -23,24 +23,28 @@ import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; 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.ObjectId;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.Signers;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NoChangesMadeException; import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.repository.Added; import sonia.scm.repository.Added;
import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.Person; 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.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.api.MergePreventReason; 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.repository.work.WorkdirProvider;
import sonia.scm.user.User; import sonia.scm.user.User;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat; 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; import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
@@ -69,14 +75,16 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Mock @Mock
private AttributeAnalyzer attributeAnalyzer; private AttributeAnalyzer attributeAnalyzer;
@Mock
private RepositoryManager repositoryManager;
@Mock
private GitRepositoryHookEventFactory eventFactory;
@BeforeClass @BeforeClass
public static void setSigner() { public static void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); Signers.set(GpgConfig.GpgFormat.OPENPGP, new GitTestHelper.SimpleGpgSigner());
} }
@Test @Test
@@ -248,27 +256,26 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt"); assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt");
} }
@Test @Test(expected = ConcurrentModificationException.class)
public void shouldHandleUnexpectedMergeResults() { public void shouldHandleConcurrentBranchModification() {
GitMergeCommand command = createCommand(git -> { GitMergeCommand command = createCommand();
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();
}
});
MergeCommandRequest request = new MergeCommandRequest(); MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("mergeable");
request.setTargetBranch("master"); request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); 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 @Test
@@ -344,6 +351,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next(); RevCommit mergeCommit = commits.iterator().next();
assertThat(mergeCommit.getParentCount()).isEqualTo(1);
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent(); PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
String message = mergeCommit.getFullMessage(); String message = mergeCommit.getFullMessage();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
@@ -370,6 +378,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
String message = mergeCommit.getFullMessage(); String message = mergeCommit.getFullMessage();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently"); assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
assertThat(message).isEqualTo("squash three commits"); assertThat(message).isEqualTo("squash three commits");
assertThat(mergeCommit.getParentCount()).isEqualTo(1);
assertThat(mergeCommit.getParent(0).name()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext()); GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext());
List<Added> changes = modificationsCommand.getModifications("master").getAdded(); List<Added> changes = modificationsCommand.getModifications("master").getAdded();
@@ -535,6 +545,32 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
assertThat(mergeCommit.getName()).doesNotStartWith("91b99de908fcd04772798a31c308a64aea1a5523"); 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 @Test
public void shouldRejectRebaseMergeIfBranchCannotBeRebased() throws IOException, GitAPIException { public void shouldRejectRebaseMergeIfBranchCannotBeRebased() throws IOException, GitAPIException {
GitMergeCommand command = createCommand(); GitMergeCommand command = createCommand();
@@ -547,11 +583,31 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandResult mergeCommandResult = command.merge(request); MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isFalse(); assertThat(mergeCommandResult.isSuccess()).isFalse();
assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt");
Repository repository = createContext().open(); Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit headCommit = commits.iterator().next(); RevCommit headCommit = commits.iterator().next();
assertThat(headCommit.getName()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec"); 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() { private GitMergeCommand createCommand() {
@@ -560,7 +616,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
} }
private GitMergeCommand createCommand(Consumer<Git> interceptor) { 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 @Override
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkingCopyFactory workingCopyFactory, String initialBranch) { <R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkingCopyFactory workingCopyFactory, String initialBranch) {
Function<Git, W> interceptedWorkerSupplier = git -> { Function<Git, W> interceptedWorkerSupplier = git -> {

View File

@@ -16,12 +16,12 @@
package sonia.scm.repository.spi; 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.Git;
import org.eclipse.jgit.api.ResetCommand;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId; 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.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
@@ -29,12 +29,12 @@ import org.junit.Test;
import sonia.scm.AlreadyExistsException; import sonia.scm.AlreadyExistsException;
import sonia.scm.BadRequestException; import sonia.scm.BadRequestException;
import sonia.scm.ConcurrentModificationException; import sonia.scm.ConcurrentModificationException;
import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.ScmConstraintViolationException; import sonia.scm.ScmConstraintViolationException;
import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.RepositoryHookType;
import sonia.scm.user.User;
import java.io.File; import java.io.File;
import java.io.IOException; 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.assertThat;
import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.fail;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.description; import static org.mockito.Mockito.description;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
public class GitModifyCommandTest extends GitModifyCommandTestBase { public class GitModifyCommandTest extends GitModifyCommandTestBase {
private static final String REALM = "AdminRealm";
@Override @Override
protected String getZippedRepositoryResource() { protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-spi-move-test.zip"; 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) @Test(expected = NotFoundException.class)
public void shouldThrowNotFoundExceptionWhenFileToDeleteDoesNotExist() { public void shouldThrowNotFoundExceptionWhenFileToDeleteDoesNotExist() {
GitModifyCommand command = createCommand(); GitModifyCommand command = createCommand();
@@ -346,10 +378,10 @@ public class GitModifyCommandTest extends GitModifyCommandTestBase {
command.execute(request); 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)); .fireHookEvent(argThat(argument -> argument.getType() == RepositoryHookType.PRE_RECEIVE));
await().pollInterval(50, MILLISECONDS).atMost(1, SECONDS).untilAsserted(() -> 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)) .fireHookEvent(argThat(argument -> argument.getType() == RepositoryHookType.POST_RECEIVE))
); );
} }
@@ -511,7 +543,7 @@ public class GitModifyCommandTest extends GitModifyCommandTestBase {
assertInTree(assertions); assertInTree(assertions);
} }
@Test(expected = AlreadyExistsException.class) @Test(expected = NoChangesMadeException.class)
public void shouldFailMoveAndKeepFilesWhenSourceAndTargetAreTheSame() { public void shouldFailMoveAndKeepFilesWhenSourceAndTargetAreTheSame() {
GitModifyCommand command = createCommand(); GitModifyCommand command = createCommand();
@@ -521,18 +553,31 @@ public class GitModifyCommandTest extends GitModifyCommandTestBase {
command.execute(request); 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() { private ModifyCommandRequest prepareModifyCommandRequest() {
ModifyCommandRequest request = new ModifyCommandRequest(); ModifyCommandRequest request = new ModifyCommandRequest();
request.setCommitMessage("Make some change"); request.setCommitMessage("Make some change");
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
return request; 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.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; 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.ObjectId;
import org.eclipse.jgit.lib.Signers;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool; import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.lfs.LfsBlobStoreFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.IOException; import java.io.IOException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; 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") @SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
class GitModifyCommandTestBase extends AbstractGitCommandTestBase { class GitModifyCommandTestBase extends AbstractGitCommandTestBase {
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
final RepositoryManager repositoryManager = mock(RepositoryManager.class);
@BeforeClass @BeforeClass
public static void setSigner() { public static void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); Signers.set(GpgConfig.GpgFormat.OPENPGP, new GitTestHelper.SimpleGpgSigner());
} }
RevCommit getLastCommit(Git git) throws GitAPIException, IOException { RevCommit getLastCommit(Git git) throws GitAPIException, IOException {
@@ -58,11 +61,23 @@ class GitModifyCommandTestBase extends AbstractGitCommandTestBase {
} }
GitModifyCommand createCommand() { 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( return new GitModifyCommand(
createContext(), createContext(),
new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider(null, repositoryLocationResolver)), new SimpleMeterRegistry()),
lfsBlobStoreFactory, 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 { void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException {

View File

@@ -68,6 +68,36 @@ public class GitModifyCommand_LFSTest extends GitModifyCommandTestBase {
assertThat(outputStream).hasToString("new content"); 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 @Test
public void shouldCreateSecondCommits() throws IOException, GitAPIException { public void shouldCreateSecondCommits() throws IOException, GitAPIException {
createCommit("new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", new ByteArrayOutputStream()); 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.getBranchProvider().getCreatedOrModified()).thenReturn(branches);
when(hookContext.getTagProvider().getCreatedTags()).thenReturn(tags); when(hookContext.getTagProvider().getCreatedTags()).thenReturn(tags);
RepositoryHookEvent event = eventFactory.createEvent( RepositoryHookEvent event = eventFactory.createPostReceiveEvent(
createContext(), createContext(),
branches, branches,
tags, tags,

View File

@@ -22,8 +22,9 @@ import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadContext;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; 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.ObjectId;
import org.eclipse.jgit.lib.Signers;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After; import org.junit.After;
@@ -81,7 +82,7 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
@Before @Before
public void setSigner() { public void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); Signers.set(GpgConfig.GpgFormat.OPENPGP, new GitTestHelper.SimpleGpgSigner());
} }
@Before @Before

View File

@@ -49,7 +49,7 @@ public class GitUnbundleCommandTest extends AbstractGitCommandTestBase {
@Test @Test
public void shouldUnbundleRepositoryFiles() throws IOException { public void shouldUnbundleRepositoryFiles() throws IOException {
RepositoryHookEvent event = new RepositoryHookEvent(null, repository, RepositoryHookType.POST_RECEIVE); 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<>(); AtomicReference<RepositoryHookEvent> receivedEvent = new AtomicReference<>();